diff --git a/.github/workflows/freertos.yml b/.github/workflows/freertos.yml new file mode 100644 index 0000000..4367339 --- /dev/null +++ b/.github/workflows/freertos.yml @@ -0,0 +1,48 @@ +# SPDX-License-Identifier: Apache-2.0 +# +# The FreeRTOS port on ESP-IDF: the sample built for a RISC-V and two Xtensa +# chips with every feature, and once with most features off. +name: freertos + +on: + pull_request: + push: + branches: [main] + +permissions: + contents: read + +jobs: + esp-idf: + name: ESP-IDF, ${{ matrix.target }}, ${{ matrix.features }} + runs-on: ubuntu-latest + container: + image: espressif/idf:v6.1 + strategy: + fail-fast: false + matrix: + include: + - target: esp32c6 + features: all + - target: esp32s3 + features: all + - target: esp32 + features: all + - target: esp32c6 + features: small + defaults: + run: + shell: bash + working-directory: ports/freertos/esp-idf/samples/minimal + steps: + - uses: actions/checkout@v4 + + - name: Build + run: | + . $IDF_PATH/export.sh + defaults="sdkconfig.defaults" + if [ "${{ matrix.features }}" = small ]; then + defaults="$defaults;sdkconfig.ci.small" + fi + idf.py -D IDF_TARGET=${{ matrix.target }} \ + -D "SDKCONFIG_DEFAULTS=$defaults" build diff --git a/README.md b/README.md index 7651226..d098c3d 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ so the client looks like a native library to the people who use that RTOS. | Port | Status | How you add it | |---|---|---| | [Zephyr](ports/zephyr/README.md) | Verified on ESP32-C6, ESP32-S3, and ESP32 | A Zephyr module: a west project, Kconfig, profiles | -| FreeRTOS (ESP-IDF first) | Planned | An ESP-IDF component; plain CMake for other FreeRTOS builds | +| [FreeRTOS](ports/freertos/README.md): ESP-IDF first | Builds for ESP32-C6, ESP32-S3, and ESP32; tested on an ESP32-C6 | An ESP-IDF component with Kconfig; the FreeRTOS kernel layer is ready for other FreeRTOS platforms | | Eclipse ThreadX | Planned | CMake library with a user configuration header | | [POSIX](ports/posix/README.md): Linux and macOS hosts; QNX and VxWorks RTP later | Run against Cumulocity on macOS; builds and passes its tests on Linux and macOS | A CMake library with CMake options | @@ -33,7 +33,11 @@ tiny-edge/ ├── ports/ │ ├── zephyr/ build glue, Kconfig, profiles, samples, tests, and the │ │ Zephyr-only sources -│ └── posix/ CMake library, options, sample, and tests +│ ├── freertos/ FreeRTOS kernel layer, and esp-idf/: the ESP-IDF +│ │ component (tiny_edge) and its sample +│ ├── posix/ CMake library, options, sample, and tests +│ └── common/ not a port: BSD sockets with mbedTLS, and the device +│ key, shared by the POSIX and ESP-IDF ports ├── provisioning/ optional library: set a device up over Improv Wi-Fi │ (include/tedge/provisioning.h) ├── tools/onboard/ tedge-onboard: set a device up from a computer @@ -57,15 +61,15 @@ configuration header, so the core builds the same way everywhere. **`ports//`** holds what does depend on the RTOS. A port is small and follows the RTOS's own conventions for: -| Concern | Zephyr port | POSIX port | Other ports | -|---|---|---|---| -| Packaging | Zephyr module, west project | CMake library | The RTOS's component or library format | -| Configuration | Kconfig `CONFIG_TEDGE_*`, mapped to `TEDGE_*` | CMake options `TEDGE_*` | `tedge_user_config.h`, in the style of `FreeRTOSConfig.h` and `tx_user.h` (ESP-IDF uses Kconfig) | -| Threads and locks | `k_thread`, `k_mutex`, work queues | pthreads | The RTOS's task and mutex API | -| Network and TLS | Zephyr sockets with TLS credentials | BSD sockets with mbedTLS | The RTOS's sockets with its usual TLS stack (mbedTLS, NetX Secure, OpenSSL) | -| Persistent settings | Settings subsystem | A file per key | NVS, a file, or the RTOS's key-value store | -| Firmware update | MCUboot | Not yet | MCUboot, or the platform's own OTA mechanism | -| Logging and shell | Zephyr logging and shell | Standard error | The RTOS's logging | +| Concern | Zephyr port | FreeRTOS port (ESP-IDF) | POSIX port | Other ports | +|---|---|---|---|---| +| Packaging | Zephyr module, west project | ESP-IDF component | CMake library | The RTOS's component or library format | +| Configuration | Kconfig `CONFIG_TEDGE_*`, mapped to `TEDGE_*` | Kconfig `CONFIG_TEDGE_*`, mapped to `TEDGE_*` | CMake options `TEDGE_*` | `tedge_user_config.h`, in the style of `FreeRTOSConfig.h` and `tx_user.h` | +| Threads and locks | `k_thread`, `k_mutex`, work queues | Static FreeRTOS tasks, semaphores, event groups, software timers | pthreads | The RTOS's task and mutex API | +| Network and TLS | Zephyr sockets with TLS credentials | lwIP's BSD sockets with mbedTLS (shared with POSIX) | BSD sockets with mbedTLS | The RTOS's sockets with its usual TLS stack (mbedTLS, NetX Secure, OpenSSL) | +| Persistent settings | Settings subsystem | NVS | A file per key | The RTOS's key-value store, or a file | +| Firmware update | MCUboot | OTA partitions with the bootloader's rollback | Not yet | MCUboot, or the platform's own OTA mechanism | +| Logging and shell | Zephyr logging and shell | `ESP_LOG`, and `esp_console` commands for the cloud shell | Standard error | The RTOS's logging | The port interface sits at the transport level: a port provides a TLS byte stream, and the core speaks MQTT, HTTP, and WebSocket over it. One protocol diff --git a/core/src/tedge_cert_renew.c b/core/src/tedge_cert_renew.c index e1f9883..de00692 100644 --- a/core/src/tedge_cert_renew.c +++ b/core/src/tedge_cert_renew.c @@ -30,6 +30,7 @@ #include #include +#include #include #include #include @@ -113,7 +114,7 @@ int tedge_cert_twin(char *buf, size_t len) strftime(iso, sizeof(iso), "%Y-%m-%dT%H:%M:%SZ", &tm); return (snprintf(buf, len, "{\"expires\":\"%s\",\"daysRemaining\":%d," - "\"renewals\":%u}", + "\"renewals\":%" PRIu32 "}", iso, days, renewals) < (int)len) ? 0 : -ENOSPC; diff --git a/core/src/tedge_parameters.c b/core/src/tedge_parameters.c index 6e43d2d..9a6793e 100644 --- a/core/src/tedge_parameters.c +++ b/core/src/tedge_parameters.c @@ -29,6 +29,7 @@ #include #include +#include #include #include #include @@ -184,7 +185,7 @@ static int check_value(const struct tedge_parameter *p, const char *raw, } if (v < p->min || v > p->max) { snprintf(reason, reason_len, - "%s: %ld is outside %d..%d", p->name, v, + "%s: %ld is outside %" PRId32 "..%" PRId32, p->name, v, p->min, p->max); return -ERANGE; } @@ -308,7 +309,7 @@ static void load_stored(struct param_set *set) case TEDGE_PARAM_TYPE_BOOL: continue; case TEDGE_PARAM_TYPE_INT: - snprintf(raw, sizeof(raw), "%d", v->v.i); + snprintf(raw, sizeof(raw), "%" PRId32, v->v.i); break; case TEDGE_PARAM_TYPE_STRING: case TEDGE_PARAM_TYPE_ENUM: @@ -375,7 +376,7 @@ static void publish(const struct param_set *set) v->v.b ? "true" : "false"); break; case TEDGE_PARAM_TYPE_INT: - n = tedge_appendf(json, size, n, "%d", v->v.i); + n = tedge_appendf(json, size, n, "%" PRId32, v->v.i); break; case TEDGE_PARAM_TYPE_STRING: case TEDGE_PARAM_TYPE_ENUM: @@ -435,7 +436,7 @@ static int check_declaration(const struct tedge_parameter *params, size_t count) TEDGE_LOG_ERR("params: '%s': min above max", p->name); return -EINVAL; } - snprintf(raw, sizeof(raw), "%d", p->def.i); + snprintf(raw, sizeof(raw), "%" PRId32, p->def.i); if (check_value(p, raw, reason, sizeof(reason)) != 0) { TEDGE_LOG_ERR("params: default %s", reason); return -EINVAL; diff --git a/ports/common/cmake/tedge_ca_bundle.cmake b/ports/common/cmake/tedge_ca_bundle.cmake new file mode 100644 index 0000000..938d7d7 --- /dev/null +++ b/ports/common/cmake/tedge_ca_bundle.cmake @@ -0,0 +1,30 @@ +# SPDX-License-Identifier: Apache-2.0 +# +# The trust anchors the core embeds (core/src/tedge_c8y.c includes +# tedge_ca_bundle.inc): every PEM file in a space-separated list, +# concatenated, as comma-separated bytes. +# +# include(/ports/common/cmake/tedge_ca_bundle.cmake) +# tedge_ca_bundle( "") +# +# A relative path is relative to the repository. A change to a file +# reconfigures the build. + +function(tedge_ca_bundle out_dir files) + get_filename_component(_root ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/../../.. ABSOLUTE) + separate_arguments(_files UNIX_COMMAND "${files}") + set(_bundle "") + foreach(_pem ${_files}) + if(NOT IS_ABSOLUTE ${_pem}) + set(_pem ${_root}/${_pem}) + endif() + if(NOT EXISTS ${_pem}) + message(FATAL_ERROR "TEDGE_C8Y_CA_FILES: no such file: ${_pem}") + endif() + file(READ ${_pem} _content HEX) + string(APPEND _bundle "${_content}") + set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS ${_pem}) + endforeach() + string(REGEX REPLACE "([0-9a-f][0-9a-f])" "0x\\1," _bundle "${_bundle}") + file(CONFIGURE OUTPUT ${out_dir}/tedge_ca_bundle.inc CONTENT "${_bundle}\n") +endfunction() diff --git a/ports/common/include/tedge_common.h b/ports/common/include/tedge_common.h new file mode 100644 index 0000000..6eef1a9 --- /dev/null +++ b/ports/common/include/tedge_common.h @@ -0,0 +1,60 @@ +/* SPDX-License-Identifier: Apache-2.0 */ + +/* What the ports share: code for platforms that offer BSD sockets and + * mbedTLS 4 (POSIX, and ESP-IDF's lwIP), and what it needs from each + * platform. Not part of the port interface, nor of the public API. + * + * - tedge_sockets.c implements the connections and TLS of + * : everything but the interface queries + * (tedge_port_net_is_up, _watch, _is_own, _on_subnet), which differ by + * platform. + * - tedge_mbedtls_key.c implements but + * tedge_port_random(): the device key, generated with PSA and kept as + * PEM where the platform keeps it, and the CSR signed with it. + */ + +#ifndef TEDGE_COMMON_H_ +#define TEDGE_COMMON_H_ + +#include + +#include + +/* Several client threads hold TLS sessions at once, and PSA's key slots + * and random generator are shared between them. */ +#if !defined(MBEDTLS_THREADING_C) +#error "tiny-edge needs mbedTLS built with MBEDTLS_THREADING_C" +#endif + +/* --- Provided by the shared code ----------------------------------------- */ + +/** Start PSA crypto, once (tedge_sockets.c). @return 0, or -EIO. */ +int tedge_common_psa_init(void); + +/** Load the device key into @p pk (initialised by the caller); only in + * builds with enrollment (tedge_mbedtls_key.c). + * @return 0; -ENOENT when there is no key yet; or another negative errno. */ +int tedge_common_device_pk(mbedtls_pk_context *pk); + +/* --- Provided by the platform -------------------------------------------- */ + +/** + * Read the stored device key, PEM, into @p pem. + * + * @return Its length (which may be more than @p cap: then it did not fit); + * -ENOENT when there is none; or another negative errno. + */ +int tedge_common_key_load(char *pem, size_t cap); + +/** Store the device key, @p len bytes of PEM, readable by the device only. + * @return 0, or a negative errno. */ +int tedge_common_key_store(const char *pem, size_t len); + +/** Where the key is kept, for log lines ("/var/lib/x/device-key.pem"). */ +const char *tedge_common_key_where(void); + +/** A socket was opened for the client: set what the platform needs on + * every one (close-on-exec, no SIGPIPE). It is non-blocking already. */ +void tedge_common_socket_opened(int fd); + +#endif /* TEDGE_COMMON_H_ */ diff --git a/ports/common/src/tedge_mbedtls_key.c b/ports/common/src/tedge_mbedtls_key.c new file mode 100644 index 0000000..da3a381 --- /dev/null +++ b/ports/common/src/tedge_mbedtls_key.c @@ -0,0 +1,138 @@ +/* SPDX-License-Identifier: Apache-2.0 + * + * The device key of with mbedTLS 4 and PSA, for any + * platform that keeps the key as PEM in its own storage (a file on POSIX, + * NVS on ESP-IDF: tedge_common_key_load() and _store()). The key is + * generated once in PSA, written out, and parsed again for each use. + * + * The platform adds tedge_port_random(). + */ + +#include +#include +#include + +#include "tedge_common.h" + +#include +#include +#include +#include + +#include +#include + +TEDGE_LOG_DECLARE(); + +/* A P-256 key in PEM is about 230 bytes. */ +#define KEY_PEM_MAX 512 + +int tedge_common_device_pk(mbedtls_pk_context *pk) +{ + unsigned char pem[KEY_PEM_MAX]; + int n, ret = tedge_common_psa_init(); + + if (ret != 0) { + return ret; + } + n = tedge_common_key_load((char *)pem, sizeof(pem) - 1); + if (n < 0) { + return n; + } + if ((size_t)n >= sizeof(pem)) { + return -EFBIG; + } + pem[n] = '\0'; + /* PEM is parsed with its NUL. */ + ret = mbedtls_pk_parse_key(pk, pem, (size_t)n + 1, NULL, 0); + mbedtls_platform_zeroize(pem, sizeof(pem)); + if (ret != 0) { + TEDGE_LOG_ERR("the device key (%s) is not usable (-0x%04x)", + tedge_common_key_where(), (unsigned int)-ret); + return -EINVAL; + } + return 0; +} + +int tedge_port_device_key_ensure(void) +{ + psa_key_attributes_t attr = PSA_KEY_ATTRIBUTES_INIT; + unsigned char pem[KEY_PEM_MAX]; + mbedtls_pk_context pk; + psa_key_id_t id = 0; + psa_status_t st; + int ret; + + mbedtls_pk_init(&pk); + ret = tedge_common_device_pk(&pk); + mbedtls_pk_free(&pk); + if (ret != -ENOENT) { + return ret; /* 0: generated on an earlier start */ + } + + /* Generated in PSA, written out once as PEM, then forgotten by PSA: + * the platform's storage is the key's only home. */ + psa_set_key_type(&attr, PSA_KEY_TYPE_ECC_KEY_PAIR(PSA_ECC_FAMILY_SECP_R1)); + psa_set_key_bits(&attr, 256); + psa_set_key_usage_flags(&attr, PSA_KEY_USAGE_SIGN_HASH | + PSA_KEY_USAGE_SIGN_MESSAGE | + PSA_KEY_USAGE_EXPORT); + psa_set_key_algorithm(&attr, PSA_ALG_ECDSA(PSA_ALG_SHA_256)); + st = psa_generate_key(&attr, &id); + psa_reset_key_attributes(&attr); + if (st != PSA_SUCCESS) { + TEDGE_LOG_ERR("generating the device key failed (%d)", (int)st); + return -EIO; + } + mbedtls_pk_init(&pk); + ret = mbedtls_pk_copy_from_psa(id, &pk); + if (ret == 0) { + ret = mbedtls_pk_write_key_pem(&pk, pem, sizeof(pem)); + } + mbedtls_pk_free(&pk); + (void)psa_destroy_key(id); + if (ret != 0) { + TEDGE_LOG_ERR("exporting the device key failed (-0x%04x)", + (unsigned int)-ret); + return -EIO; + } + ret = tedge_common_key_store((const char *)pem, strlen((char *)pem)); + mbedtls_platform_zeroize(pem, sizeof(pem)); + if (ret != 0) { + TEDGE_LOG_ERR("could not store the device key in %s (%d)", + tedge_common_key_where(), ret); + return ret; + } + TEDGE_LOG_INF("device key generated (P-256, %s)", tedge_common_key_where()); + return 0; +} + +int tedge_port_device_csr(const char *subject, char *pem, size_t cap) +{ + mbedtls_x509write_csr csr; + mbedtls_pk_context pk; + int ret; + + mbedtls_x509write_csr_init(&csr); + mbedtls_pk_init(&pk); + ret = tedge_common_device_pk(&pk); + if (ret != 0) { + goto out; + } + mbedtls_x509write_csr_set_md_alg(&csr, MBEDTLS_MD_SHA256); + mbedtls_x509write_csr_set_key(&csr, &pk); + ret = mbedtls_x509write_csr_set_subject_name(&csr, subject); + if (ret == 0) { + ret = mbedtls_x509write_csr_pem(&csr, (unsigned char *)pem, cap); + } + if (ret != 0) { + TEDGE_LOG_ERR("writing the CSR failed (-0x%04x)", (unsigned int)-ret); + ret = -EIO; + goto out; + } + ret = (int)strlen(pem); +out: + mbedtls_pk_free(&pk); + mbedtls_x509write_csr_free(&csr); + return ret; +} diff --git a/ports/common/src/tedge_sockets.c b/ports/common/src/tedge_sockets.c new file mode 100644 index 0000000..8c42a39 --- /dev/null +++ b/ports/common/src/tedge_sockets.c @@ -0,0 +1,664 @@ +/* SPDX-License-Identifier: Apache-2.0 + * + * Connections for on any platform with BSD sockets and + * mbedTLS 4: POSIX systems, and ESP-IDF's lwIP. The platform adds the + * interface queries (up, watch, own address, subnet). + * + * A connection handle is a slot in a table of this file's own, not the + * socket itself: a TLS connection carries its mbedTLS session beside the + * socket. Each slot is used by one thread at a time (the interface's + * rule), so only taking and freeing slots needs the table's lock. + * + * Every socket is non-blocking, and every wait is a poll() with the time + * left, so a timeout holds whether TLS or the kernel is waiting. + */ + +#include +#include +#include +#include + +#include "tedge_common.h" + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#if __has_include() +#include +#else +#include /* ESP-IDF */ +#endif +#include +#include +#include + +TEDGE_LOG_DECLARE(); + +/* More than the client holds at once: MQTT, a download or an upload, + * enrollment, and the remote-access sessions with their targets. */ +#define MAX_CONNS 16 +/* How long a send may wait for room in the socket before the connection is + * given up as dead. */ +#define SEND_TIMEOUT_MS 30000 + +#if defined(MSG_NOSIGNAL) +#define SEND_FLAGS MSG_NOSIGNAL +#else +#define SEND_FLAGS 0 /* SO_NOSIGPIPE on the socket instead (macOS) */ +#endif + +struct conn { + bool used; + bool tls; + int fd; + mbedtls_ssl_context ssl; + mbedtls_ssl_config conf; + mbedtls_x509_crt ca; + mbedtls_x509_crt own; + mbedtls_pk_context key; +}; + +static struct conn conns[MAX_CONNS]; +TEDGE_PORT_MUTEX_DEFINE(conns_lock); + +static int64_t now_ms(void) +{ + return tedge_port_uptime_ms(); +} + +static struct conn *conn_get(int handle) +{ + if (handle < 0 || handle >= MAX_CONNS || !conns[handle].used) { + return NULL; + } + return &conns[handle]; +} + +static int conn_alloc(int fd, bool tls) +{ + int handle = -ENFILE; + + tedge_port_mutex_lock(&conns_lock); + for (int i = 0; i < MAX_CONNS; i++) { + if (!conns[i].used) { + conns[i].used = true; + conns[i].tls = tls; + conns[i].fd = fd; + if (tls) { + /* Before anything can fail, so conn_free() + * always frees initialised state. */ + mbedtls_ssl_init(&conns[i].ssl); + mbedtls_ssl_config_init(&conns[i].conf); + mbedtls_x509_crt_init(&conns[i].ca); + mbedtls_x509_crt_init(&conns[i].own); + mbedtls_pk_init(&conns[i].key); + } + handle = i; + break; + } + } + tedge_port_mutex_unlock(&conns_lock); + return handle; +} + +static void conn_free(struct conn *c) +{ + if (c->tls) { + mbedtls_ssl_free(&c->ssl); + mbedtls_ssl_config_free(&c->conf); + mbedtls_x509_crt_free(&c->ca); + mbedtls_x509_crt_free(&c->own); + mbedtls_pk_free(&c->key); + } + if (c->fd >= 0) { + (void)close(c->fd); + } + tedge_port_mutex_lock(&conns_lock); + c->used = false; + c->tls = false; + c->fd = -1; + tedge_port_mutex_unlock(&conns_lock); +} + +/* A socket as every connection here has it: non-blocking, and whatever + * else the platform sets on each (tedge_common_socket_opened()). */ +static int socket_open(int type) +{ + int fd = socket(AF_INET, type, 0); + + if (fd < 0) { + return -errno; + } + (void)fcntl(fd, F_SETFL, fcntl(fd, F_GETFL) | O_NONBLOCK); + tedge_common_socket_opened(fd); + return fd; +} + +/* Wait until @p fd is ready for @p events or @p deadline passes. + * @return 0 when ready, -EAGAIN at the deadline, or a negative errno. */ +static int wait_fd(int fd, short events, int64_t deadline) +{ + for (;;) { + struct pollfd pfd = { .fd = fd, .events = events }; + int64_t left = deadline - now_ms(); + int ret; + + if (left <= 0) { + return -EAGAIN; + } + ret = poll(&pfd, 1, (int)left); + if (ret > 0) { + return 0; + } + if (ret == 0) { + return -EAGAIN; + } + if (errno != EINTR) { + return -errno; + } + } +} + +/* --- Names --------------------------------------------------------------- */ + +bool tedge_port_ipv4_parse(const char *text, uint8_t addr[4]) +{ + struct in_addr in; + + if (inet_pton(AF_INET, text, &in) != 1) { + return false; + } + memcpy(addr, &in, 4); + return true; +} + +/* getaddrinfo(), which follows the system's configuration (resolv.conf, + * nsswitch.conf, mDNS on macOS; lwIP's DNS client). It cannot be + * cancelled, so the timeout is the resolver's own, not @p timeout_ms. */ +int tedge_port_resolve(const char *host, uint8_t addr[4], uint32_t timeout_ms) +{ + struct addrinfo hints = { .ai_family = AF_INET, + .ai_socktype = SOCK_STREAM }; + struct addrinfo *res; + int ret; + + (void)timeout_ms; + if (tedge_port_ipv4_parse(host, addr)) { + return 0; + } + ret = getaddrinfo(host, NULL, &hints, &res); + switch (ret) { + case 0: + memcpy(addr, &((struct sockaddr_in *)(void *)res->ai_addr)->sin_addr, + 4); + freeaddrinfo(res); + return 0; +#if defined(EAI_AGAIN) + case EAI_AGAIN: /* the servers did not answer */ + return -ETIMEDOUT; +#endif +#if defined(EAI_SYSTEM) + case EAI_SYSTEM: +#endif + case EAI_MEMORY: + return -EAGAIN; +#if defined(TEDGE_COMMON_LWIP) + /* lwIP reports a timeout and a name without an address alike. */ + case EAI_FAIL: + return -ETIMEDOUT; +#endif + default: /* EAI_NONAME, EAI_FAIL, ...: the next try says the same */ + return -ENOENT; + } +} + +/* --- PSA ---------------------------------------------------------------- */ + +/* mbedTLS 4 runs its TLS on PSA crypto, which must be started first. The + * device key (tedge_mbedtls_key.c) needs it too. */ +TEDGE_PORT_MUTEX_DEFINE(psa_lock); +static bool psa_started; + +int tedge_common_psa_init(void) +{ + psa_status_t st = PSA_SUCCESS; + + tedge_port_mutex_lock(&psa_lock); + if (!psa_started) { + st = psa_crypto_init(); + psa_started = (st == PSA_SUCCESS); + } + tedge_port_mutex_unlock(&psa_lock); + if (st != PSA_SUCCESS) { + TEDGE_LOG_ERR("PSA crypto did not start (%d)", (int)st); + return -EIO; + } + return 0; +} + +/* --- TLS credentials ----------------------------------------------------- */ + +/* The buffers stay the caller's (the interface's rule). Each TLS + * connection parses what is set when it opens, so a change of credentials + * never pulls them from under a session. */ +TEDGE_PORT_MUTEX_DEFINE(cred_lock); +static const void *ca_pem; +static size_t ca_len; +static const void *device_cert; +static size_t device_cert_len; + +int tedge_port_tls_set_ca(const void *pem, size_t len) +{ + tedge_port_mutex_lock(&cred_lock); + ca_pem = pem; + ca_len = len; + tedge_port_mutex_unlock(&cred_lock); + return 0; +} + +int tedge_port_tls_set_device(const void *cert, size_t cert_len) +{ + tedge_port_mutex_lock(&cred_lock); + device_cert = cert; + device_cert_len = cert_len; + tedge_port_mutex_unlock(&cred_lock); + return 0; +} + +void tedge_port_tls_clear_device(void) +{ + tedge_port_mutex_lock(&cred_lock); + device_cert = NULL; + device_cert_len = 0; + tedge_port_mutex_unlock(&cred_lock); +} + +/* --- TLS over a socket --------------------------------------------------- */ + +static int bio_send(void *ctx, const unsigned char *buf, size_t len) +{ + struct conn *c = ctx; + ssize_t n = send(c->fd, buf, len, SEND_FLAGS); + + if (n >= 0) { + return (int)n; + } + if (errno == EAGAIN || errno == EWOULDBLOCK || errno == EINTR) { + return MBEDTLS_ERR_SSL_WANT_WRITE; + } + return (errno == EPIPE || errno == ECONNRESET) ? MBEDTLS_ERR_NET_CONN_RESET + : MBEDTLS_ERR_NET_SEND_FAILED; +} + +static int bio_recv(void *ctx, unsigned char *buf, size_t len) +{ + struct conn *c = ctx; + ssize_t n = recv(c->fd, buf, len, 0); + + if (n >= 0) { + return (int)n; + } + if (errno == EAGAIN || errno == EWOULDBLOCK || errno == EINTR) { + return MBEDTLS_ERR_SSL_WANT_READ; + } + return (errno == ECONNRESET) ? MBEDTLS_ERR_NET_CONN_RESET + : MBEDTLS_ERR_NET_RECV_FAILED; +} + +static int tls_setup(struct conn *c, enum tedge_port_security security, + const char *hostname) +{ + const char *what = "CA bundle"; + int ret; + + ret = tedge_common_psa_init(); + if (ret != 0) { + return ret; + } + tedge_port_mutex_lock(&cred_lock); + ret = (ca_pem != NULL) + ? mbedtls_x509_crt_parse(&c->ca, ca_pem, ca_len) + : MBEDTLS_ERR_X509_BAD_INPUT_DATA; + if (ret == 0 && security == TEDGE_PORT_TLS_DEVICE) { + what = "device certificate"; + ret = (device_cert != NULL) + ? mbedtls_x509_crt_parse_der(&c->own, device_cert, + device_cert_len) + : MBEDTLS_ERR_X509_BAD_INPUT_DATA; + } + tedge_port_mutex_unlock(&cred_lock); + if (ret != 0) { + TEDGE_LOG_ERR("TLS: the %s is missing or not valid (-0x%04x)", + what, (unsigned int)-ret); + return -EINVAL; + } + if (security == TEDGE_PORT_TLS_DEVICE) { +#if defined(TEDGE_AUTH_C8Y_CA) + ret = tedge_common_device_pk(&c->key); +#else + ret = -ENOTSUP; /* only enrollment gives the device a key */ +#endif + if (ret != 0) { + return ret; + } + } + + ret = mbedtls_ssl_config_defaults(&c->conf, MBEDTLS_SSL_IS_CLIENT, + MBEDTLS_SSL_TRANSPORT_STREAM, + MBEDTLS_SSL_PRESET_DEFAULT); + if (ret != 0) { + return -ENOMEM; + } + /* TLS 1.2, as the Zephyr port connects. */ + mbedtls_ssl_conf_max_tls_version(&c->conf, MBEDTLS_SSL_VERSION_TLS1_2); + mbedtls_ssl_conf_authmode(&c->conf, MBEDTLS_SSL_VERIFY_REQUIRED); + mbedtls_ssl_conf_ca_chain(&c->conf, &c->ca, NULL); + if (security == TEDGE_PORT_TLS_DEVICE && + mbedtls_ssl_conf_own_cert(&c->conf, &c->own, &c->key) != 0) { + return -ENOMEM; + } + if (mbedtls_ssl_setup(&c->ssl, &c->conf) != 0 || + mbedtls_ssl_set_hostname(&c->ssl, hostname) != 0) { + return -ENOMEM; + } + mbedtls_ssl_set_bio(&c->ssl, c, bio_send, bio_recv, NULL); + return 0; +} + +static int tls_handshake(struct conn *c, const char *hostname, int64_t deadline) +{ + for (;;) { + int ret = mbedtls_ssl_handshake(&c->ssl); + + if (ret == 0) { + return 0; + } + if (ret == MBEDTLS_ERR_SSL_WANT_READ || + ret == MBEDTLS_ERR_SSL_WANT_WRITE) { + ret = wait_fd(c->fd, + (ret == MBEDTLS_ERR_SSL_WANT_READ) ? POLLIN + : POLLOUT, + deadline); + if (ret == -EAGAIN) { + TEDGE_LOG_WRN("TLS: %s: the handshake timed out", + hostname); + return -ETIMEDOUT; + } + if (ret != 0) { + return ret; + } + continue; + } + if (ret == MBEDTLS_ERR_X509_CERT_VERIFY_FAILED) { + char info[160]; + + (void)mbedtls_x509_crt_verify_info( + info, sizeof(info), "", + mbedtls_ssl_get_verify_result(&c->ssl)); + info[strcspn(info, "\n")] = '\0'; + TEDGE_LOG_ERR("TLS: %s: the server's certificate is not " + "trusted: %s", hostname, info); + return -EACCES; + } + TEDGE_LOG_WRN("TLS: %s: the handshake failed (-0x%04x)", hostname, + (unsigned int)-ret); + return -ECONNABORTED; + } +} + +/* --- Connections --------------------------------------------------------- */ + +int tedge_port_connect(const uint8_t addr[4], uint16_t port, + enum tedge_port_security security, const char *hostname, + uint32_t timeout_ms) +{ + struct sockaddr_in sa = { .sin_family = AF_INET, + .sin_port = htons(port) }; + int64_t deadline = now_ms() + timeout_ms; + bool tls = security != TEDGE_PORT_TCP; + struct conn *c; + int fd, handle, ret; + + memcpy(&sa.sin_addr, addr, 4); + fd = socket_open(SOCK_STREAM); + if (fd < 0) { + return fd; + } + handle = conn_alloc(fd, tls); + if (handle < 0) { + (void)close(fd); + return handle; + } + c = &conns[handle]; + + if (connect(fd, (struct sockaddr *)&sa, sizeof(sa)) != 0) { + if (errno != EINPROGRESS) { + ret = -errno; + goto fail; + } + ret = wait_fd(fd, POLLOUT, deadline); + if (ret != 0) { + ret = (ret == -EAGAIN) ? -ETIMEDOUT : ret; + goto fail; + } + { + int err = 0; + socklen_t len = sizeof(err); + + (void)getsockopt(fd, SOL_SOCKET, SO_ERROR, &err, &len); + if (err != 0) { + ret = -err; + goto fail; + } + } + } + if (tls) { + ret = tls_setup(c, security, hostname); + if (ret == 0) { + ret = tls_handshake(c, hostname, deadline); + } + if (ret != 0) { + goto fail; + } + } + return handle; + +fail: + conn_free(c); + return ret; +} + +int tedge_port_udp_open(void) +{ + int fd = socket_open(SOCK_DGRAM); + int handle; + + if (fd < 0) { + return fd; + } + handle = conn_alloc(fd, false); + if (handle < 0) { + (void)close(fd); + } + return handle; +} + +int tedge_port_udp_sendto(int conn, const void *buf, size_t len, + const uint8_t addr[4], uint16_t port) +{ + struct sockaddr_in sa = { .sin_family = AF_INET, + .sin_port = htons(port) }; + struct conn *c = conn_get(conn); + + if (c == NULL) { + return -EBADF; + } + memcpy(&sa.sin_addr, addr, 4); + return (sendto(c->fd, buf, len, SEND_FLAGS, (struct sockaddr *)&sa, + sizeof(sa)) < 0) ? -errno : 0; +} + +int tedge_port_set_nodelay(int conn) +{ + struct conn *c = conn_get(conn); + int on = 1; + + if (c == NULL) { + return -EBADF; + } + return (setsockopt(c->fd, IPPROTO_TCP, TCP_NODELAY, &on, sizeof(on)) < 0) + ? -errno : 0; +} + +int tedge_port_send(int conn, const void *buf, size_t len) +{ + int64_t deadline = now_ms() + SEND_TIMEOUT_MS; + struct conn *c = conn_get(conn); + + if (c == NULL) { + return -EBADF; + } + for (;;) { + short want = POLLOUT; + int ret; + + if (c->tls) { + ret = mbedtls_ssl_write(&c->ssl, buf, len); + if (ret >= 0) { + return ret; + } + if (ret == MBEDTLS_ERR_SSL_WANT_READ) { + want = POLLIN; + } else if (ret != MBEDTLS_ERR_SSL_WANT_WRITE) { + return -ECONNRESET; + } + } else { + ssize_t n = send(c->fd, buf, len, SEND_FLAGS); + + if (n >= 0) { + return (int)n; + } + if (errno != EAGAIN && errno != EWOULDBLOCK && + errno != EINTR) { + return -errno; + } + } + ret = wait_fd(c->fd, want, deadline); + if (ret != 0) { + return (ret == -EAGAIN) ? -ETIMEDOUT : ret; + } + } +} + +int tedge_port_recv(int conn, void *buf, size_t len, uint32_t timeout_ms) +{ + int64_t deadline = now_ms() + timeout_ms; + struct conn *c = conn_get(conn); + + if (c == NULL) { + return -EBADF; + } + for (;;) { + short want = POLLIN; + int ret; + + if (c->tls) { + ret = mbedtls_ssl_read(&c->ssl, buf, len); + if (ret >= 0) { + return ret; /* 0: the peer closed */ + } + if (ret == MBEDTLS_ERR_SSL_PEER_CLOSE_NOTIFY) { + return 0; + } + if (ret == MBEDTLS_ERR_SSL_WANT_WRITE) { + want = POLLOUT; + } else if (ret != MBEDTLS_ERR_SSL_WANT_READ) { + return -ECONNRESET; + } + } else { + ssize_t n = recv(c->fd, buf, len, 0); + + if (n >= 0) { + return (int)n; + } + if (errno != EAGAIN && errno != EWOULDBLOCK && + errno != EINTR) { + return -errno; + } + } + if (timeout_ms == 0) { + return -EAGAIN; + } + ret = wait_fd(c->fd, want, deadline); + if (ret != 0) { + return ret; + } + } +} + +int tedge_port_poll(struct tedge_port_pollfd *fds, size_t n, + uint32_t timeout_ms) +{ + struct pollfd pfds[8]; + int buffered = 0; + int ret; + + if (n > sizeof(pfds) / sizeof(pfds[0])) { + return -EINVAL; + } + for (size_t i = 0; i < n; i++) { + struct conn *c = conn_get(fds[i].conn); + + fds[i].revents = 0; + pfds[i].fd = (c != NULL) ? c->fd : -1; + pfds[i].events = (fds[i].events & TEDGE_PORT_POLLIN) ? POLLIN : 0; + pfds[i].revents = 0; + /* TLS may hold a decrypted record the socket no longer shows. */ + if (c != NULL && c->tls && (fds[i].events & TEDGE_PORT_POLLIN) && + mbedtls_ssl_get_bytes_avail(&c->ssl) > 0) { + fds[i].revents = TEDGE_PORT_POLLIN; + buffered++; + } + } + do { + ret = poll(pfds, n, buffered > 0 ? 0 : (int)timeout_ms); + } while (ret < 0 && errno == EINTR); + if (ret < 0) { + return -errno; + } + ret = 0; + for (size_t i = 0; i < n; i++) { + fds[i].revents |= + ((pfds[i].revents & POLLIN) ? TEDGE_PORT_POLLIN : 0) | + ((pfds[i].revents & (POLLERR | POLLNVAL)) ? TEDGE_PORT_POLLERR + : 0) | + ((pfds[i].revents & POLLHUP) ? TEDGE_PORT_POLLHUP : 0); + if (fds[i].revents != 0) { + ret++; + } + } + return ret; +} + +void tedge_port_close(int conn) +{ + struct conn *c = conn_get(conn); + + if (c == NULL) { + return; + } + if (c->tls) { + /* Best effort: the socket is non-blocking, and a peer that is + * gone does not hold the close up. */ + (void)mbedtls_ssl_close_notify(&c->ssl); + } + conn_free(c); +} diff --git a/ports/freertos/README.md b/ports/freertos/README.md new file mode 100644 index 0000000..81e1353 --- /dev/null +++ b/ports/freertos/README.md @@ -0,0 +1,161 @@ +# tiny-edge FreeRTOS port + +The thin-edge.io client on FreeRTOS. ESP-IDF is the first platform: an +ESP-IDF component, `tiny_edge`, for ESP32 applications. It is the same +client as on Zephyr and POSIX: the same core, the same options, and the same +behavior in Cumulocity. + +## Layout + +``` +ports/freertos/ +├── include/tedge_port/os.h kernel types for any FreeRTOS platform +├── src/tedge_port_os.c kernel services for any FreeRTOS platform +└── esp-idf/ + ├── tiny_edge/ the ESP-IDF component + │ ├── Kconfig the client's options (CONFIG_TEDGE_*) + │ ├── include/ ESP_LOG mapping + │ └── src/ NVS, esp_netif, SNTP, OTA, device glue + └── samples/minimal/ Wi-Fi, the client, and a measurement +``` + +The ESP-IDF build takes its sources from four places: + +| Layer | Directory | Used by | +|---|---|---| +| The client | `core/` | Every port | +| BSD sockets with mbedTLS 4, and the device key | `ports/common/` | POSIX and ESP-IDF | +| FreeRTOS kernel services | `ports/freertos/src/` | Every FreeRTOS platform | +| ESP-IDF services | `ports/freertos/esp-idf/tiny_edge/src/` | ESP-IDF | + +About 700 lines are ESP-IDF's own. Everything else is shared. + +The kernel layer uses only the FreeRTOS API, so another FreeRTOS platform +(an STM32 with lwIP, for example) reuses it and `ports/common/`. Such a +platform then adds settings storage, the network interface queries, the +clock, and its firmware slots. + +## Use it in an application + +ESP-IDF 6.1 or later. Add the component with the component manager, in your +`main/idf_component.yml`: + +```yaml +dependencies: + tiny_edge: + git: https://github.com/thin-edge/tiny-edge.git + path: ports/freertos/esp-idf/tiny_edge +``` + +Then add `REQUIRES tiny_edge` (or `PRIV_REQUIRES`) to your component, and +start the client once the network is set up: + +```c +#include + +tedge_init(NULL, &hooks); +tedge_start(); +``` + +The application owns Wi-Fi or Ethernet, NVS, and the default event loop, as +in any ESP-IDF application. The client watches the default network +interface, and reconnects when it comes back. + +## Configuration + +The options are in Kconfig: `idf.py menuconfig`, then "tiny-edge +(thin-edge.io client)", or `CONFIG_TEDGE_*` lines in `sdkconfig.defaults`. +The names and defaults match the other ports. The core sees +`CONFIG_TEDGE_HEALTH` as `TEDGE_HEALTH`. + +| ESP-IDF setting | Why | +|---|---| +| A partition table with two OTA app partitions | Firmware update writes the new image to the other one | +| `CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE=y` | An image that does not confirm itself is rolled back. The build stops without it when firmware update is on | +| `CONFIG_FREERTOS_TIMER_TASK_STACK_DEPTH=3072` | The firmware confirmation deadline runs on the timer task | + +The component turns on the mbedTLS options it needs: threading, and CSR +writing for enrollment. + +## How the port maps the interface + +| Interface | ESP-IDF | +|---|---| +| Threads, mutexes, queues, events, semaphores | `xTaskCreateStatic`, `xSemaphoreCreate*Static`, `xQueueCreateStatic`, `xEventGroupCreateStatic`: nothing from the FreeRTOS heap | +| Delayed work | A one-shot FreeRTOS software timer | +| The client's heap | An allowance of `CONFIG_TEDGE_HEAP_SIZE` on `pvPortMalloc` | +| Network | lwIP's BSD sockets, mbedTLS 4.1 for TLS (`ports/common/`), and `esp_netif` and its IP events for the interface | +| Settings | NVS, namespace `tedge`. A key longer than NVS allows is stored under its hash, with the full key beside the value | +| Device key | P-256, generated with PSA, kept as PEM in NVS (`tedge/device-key`). Turn on NVS encryption on a production device | +| Clock | `esp_netif_sntp`, unless the clock is already set | +| Firmware update | `esp_ota_*` with the bootloader's rollback | +| Logging | `ESP_LOG`, tag `tedge`. With log upload, every line the image logs is also kept for upload, cut at 127 bytes | +| Device id | `tedge` and the MAC of the default interface | +| Shell commands (`c8y_Command`) | The application's `esp_console` commands, run with `esp_console_run()` on a thread of their own. What the command's task prints to stdout is the result | + +## Shell commands + +The cloud's shell (`c8y_Command`) runs the application's own console +commands, the ones it registers with `esp_console_cmd_register()`. Nothing +runs until `CONFIG_TEDGE_SHELL_COMMAND_ALLOW_LIST` names what may, for +example `"free,version"`: a command must begin with one of those, and +anything that could chain a second command is refused. `help` answers with +the list. + +The result is what the command printed. While a command runs, stdout keeps +only what the command's task prints and passes everything else on to the +console. A command that does not return within +`CONFIG_TEDGE_SHELL_COMMAND_TIMEOUT_S` is reported as failed, but keeps its +thread until it returns, so keep commands that can block off the list. + +The sample registers `free`, `version`, and `uptime`, and allows those three. + +## Build the sample + +With the ESP-IDF container (or a local ESP-IDF 6.1): + +```sh +cd ports/freertos/esp-idf/samples/minimal +idf.py set-target esp32c6 +idf.py menuconfig # "tiny-edge sample": Wi-Fi; "tiny-edge": the tenant +idf.py build flash monitor +``` + +The sample keeps the Wi-Fi network on the device. An image built without +one, such as an image for a firmware update, uses the stored network, so it +carries no Wi-Fi password. + +On the first boot the sample logs a registration URL. Open it to register +the device in Cumulocity. The key and certificate stay in NVS, so later +boots connect at once. + +## Not in this port yet + +- Crash dumps: ESP-IDF's core dump partition would be offered as a log type. +- The provisioning library (Improv over serial or BLE). +- Plain FreeRTOS builds outside ESP-IDF: the kernel layer is ready, but no + platform layer or CMake glue exists yet. + +## Status + +Builds without warnings for the ESP32-C6, the ESP32-S3, and the ESP32, with +every feature and with a reduced set (bootstrap authentication, telemetry +only). The sample with every feature is 1.06 MB on the ESP32-C6, 61% of a +1.75 MB OTA slot. + +Tested on an ESP32-C6 against a Cumulocity tenant: certificate enrollment +from the registration URL, the MQTT connection, measurements, the health +measurement, log upload, a remote access tunnel, an operation +interrupted by a reset (reported as failed after the reboot), parameter +changes (kept across a reboot), shell commands, a firmware update from the +tenant's firmware repository (the new image confirmed itself once +connected), and a rollback (an image that never connected was reset after +its confirmation deadline, and the bootloader went back to the previous +one, which reported the update as failed). + +Each thread that runs a TLS handshake of its own (log upload, firmware +download, remote access) has a 12 KB stack: the log upload used 8.1 KB on +the ESP32-C6, more than with Zephyr's mbedTLS. In a build with +`CONFIG_TEDGE_LOG_LEVEL=4` and `CONFIG_LOG_MAXIMUM_LEVEL_DEBUG=y` (ESP-IDF +leaves out debug lines otherwise), and the client's `log_level` parameter at +`dbg`, each thread logs how much of its stack it never used when it ends. diff --git a/ports/freertos/esp-idf/samples/minimal/.gitignore b/ports/freertos/esp-idf/samples/minimal/.gitignore new file mode 100644 index 0000000..a5c27a6 --- /dev/null +++ b/ports/freertos/esp-idf/samples/minimal/.gitignore @@ -0,0 +1,5 @@ +build/ +managed_components/ +dependencies.lock +sdkconfig +sdkconfig.old diff --git a/ports/freertos/esp-idf/samples/minimal/CMakeLists.txt b/ports/freertos/esp-idf/samples/minimal/CMakeLists.txt new file mode 100644 index 0000000..c602daa --- /dev/null +++ b/ports/freertos/esp-idf/samples/minimal/CMakeLists.txt @@ -0,0 +1,12 @@ +# SPDX-License-Identifier: Apache-2.0 +# +# The smallest ESP-IDF application that hosts tiny-edge: join Wi-Fi, start +# the client, and publish a measurement now and then. +# +# idf.py set-target esp32c6 +# idf.py menuconfig # Wi-Fi and the tenant: "tiny-edge sample" +# idf.py build flash monitor + +cmake_minimum_required(VERSION 3.22) +include($ENV{IDF_PATH}/tools/cmake/project.cmake) +project(tedge-minimal) diff --git a/ports/freertos/esp-idf/samples/minimal/main/CMakeLists.txt b/ports/freertos/esp-idf/samples/minimal/main/CMakeLists.txt new file mode 100644 index 0000000..a8902e3 --- /dev/null +++ b/ports/freertos/esp-idf/samples/minimal/main/CMakeLists.txt @@ -0,0 +1,4 @@ +# SPDX-License-Identifier: Apache-2.0 +idf_component_register(SRCS main.c + PRIV_REQUIRES tiny_edge esp_wifi esp_netif esp_event nvs_flash console + esp_app_format) diff --git a/ports/freertos/esp-idf/samples/minimal/main/Kconfig.projbuild b/ports/freertos/esp-idf/samples/minimal/main/Kconfig.projbuild new file mode 100644 index 0000000..124c8c2 --- /dev/null +++ b/ports/freertos/esp-idf/samples/minimal/main/Kconfig.projbuild @@ -0,0 +1,19 @@ +menu "tiny-edge sample" + +config SAMPLE_WIFI_SSID + string "Wi-Fi network (SSID)" + default "" + help + Stored on the device when it starts. An image built without one + (for a firmware update, say) uses the network stored there, so the + image carries no Wi-Fi password. + +config SAMPLE_WIFI_PASSWORD + string "Wi-Fi password" + default "" + +config SAMPLE_MEASUREMENT_INTERVAL_S + int "Seconds between measurements" + default 60 + +endmenu diff --git a/ports/freertos/esp-idf/samples/minimal/main/idf_component.yml b/ports/freertos/esp-idf/samples/minimal/main/idf_component.yml new file mode 100644 index 0000000..c36f008 --- /dev/null +++ b/ports/freertos/esp-idf/samples/minimal/main/idf_component.yml @@ -0,0 +1,9 @@ +# The tiny-edge component from this checkout. An application outside the +# repository names it by git instead: +# +# tiny_edge: +# git: https://github.com/thin-edge/tiny-edge.git +# path: ports/freertos/esp-idf/tiny_edge +dependencies: + tiny_edge: + path: ../../../tiny_edge diff --git a/ports/freertos/esp-idf/samples/minimal/main/main.c b/ports/freertos/esp-idf/samples/minimal/main/main.c new file mode 100644 index 0000000..996fb72 --- /dev/null +++ b/ports/freertos/esp-idf/samples/minimal/main/main.c @@ -0,0 +1,187 @@ +/* SPDX-License-Identifier: Apache-2.0 + * + * The smallest ESP-IDF application that hosts the thin-edge.io client: + * join Wi-Fi, start the client, and publish a measurement now and then. + * The client connects, enrolls and reports by itself. + * + * With certificate enrollment (the default), the first boot logs a URL: + * open it to register the device in Cumulocity. The key and certificate + * are kept in NVS, so later boots connect at once. + */ + +#include + +#include +#include +#include +#include +#include +#include +#include +#if CONFIG_TEDGE_SHELL_COMMAND +#include +#include +#include +#endif + +#include + +static const char *TAG = "sample"; + +static void on_state(enum tedge_state state, void *user_data) +{ + (void)user_data; + if (state == TEDGE_STATE_AWAITING_REGISTRATION) { + char url[220]; + + /* The URL carries a one-time password: it is for the person + * at the console. */ + if (tedge_registration_url(url, sizeof(url)) > 0) { + ESP_LOGI(TAG, "register this device: %s", url); + } + } +} + +static const struct tedge_hooks hooks = { + .on_state = on_state, +}; + +/* Keeps the station connected: the client only watches the network. */ +static void on_wifi(void *arg, esp_event_base_t base, int32_t id, void *data) +{ + (void)arg; + if (base == WIFI_EVENT && id == WIFI_EVENT_STA_START) { + (void)esp_wifi_connect(); + } else if (base == WIFI_EVENT && id == WIFI_EVENT_STA_DISCONNECTED) { + const wifi_event_sta_disconnected_t *ev = data; + + ESP_LOGW(TAG, "Wi-Fi: not connected (reason %d), trying again", + ev->reason); + vTaskDelay(pdMS_TO_TICKS(1000)); + (void)esp_wifi_connect(); + } else if (base == IP_EVENT && id == IP_EVENT_STA_GOT_IP) { + const ip_event_got_ip_t *ev = data; + + ESP_LOGI(TAG, "Wi-Fi: address " IPSTR, IP2STR(&ev->ip_info.ip)); + } +} + +static void wifi_start(void) +{ + wifi_init_config_t init = WIFI_INIT_CONFIG_DEFAULT(); + wifi_config_t cfg = { 0 }; + + esp_netif_create_default_wifi_sta(); + ESP_ERROR_CHECK(esp_wifi_init(&init)); + ESP_ERROR_CHECK(esp_event_handler_register(WIFI_EVENT, ESP_EVENT_ANY_ID, + on_wifi, NULL)); + ESP_ERROR_CHECK(esp_event_handler_register(IP_EVENT, IP_EVENT_STA_GOT_IP, + on_wifi, NULL)); + ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA)); + if (CONFIG_SAMPLE_WIFI_SSID[0] != '\0') { + strncpy((char *)cfg.sta.ssid, CONFIG_SAMPLE_WIFI_SSID, + sizeof(cfg.sta.ssid)); + strncpy((char *)cfg.sta.password, CONFIG_SAMPLE_WIFI_PASSWORD, + sizeof(cfg.sta.password)); + /* ESP-IDF keeps it in NVS, for images built without one. */ + ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &cfg)); + } else if (esp_wifi_get_config(WIFI_IF_STA, &cfg) != ESP_OK || + cfg.sta.ssid[0] == '\0') { + ESP_LOGE(TAG, "no Wi-Fi network: set one in menuconfig " + "(\"tiny-edge sample\")"); + } else { + ESP_LOGI(TAG, "Wi-Fi: using the network stored on the device"); + } + ESP_ERROR_CHECK(esp_wifi_start()); +} + +#if CONFIG_TEDGE_SHELL_COMMAND +/* Commands the cloud can run (c8y_Command), once they are on + * CONFIG_TEDGE_SHELL_COMMAND_ALLOW_LIST. What they print is the result. */ +static int cmd_free(int argc, char **argv) +{ + (void)argc; + (void)argv; + printf("heap: %u bytes free, %u at the lowest\n", + (unsigned int)esp_get_free_heap_size(), + (unsigned int)esp_get_minimum_free_heap_size()); + printf("largest free block: %u bytes\n", + (unsigned int)heap_caps_get_largest_free_block(MALLOC_CAP_DEFAULT)); + return 0; +} + +static int cmd_version(int argc, char **argv) +{ + const esp_app_desc_t *app = esp_app_get_description(); + + (void)argc; + (void)argv; + printf("%s %s, tiny-edge %s, ESP-IDF %s\n", app->project_name, + app->version, tedge_version(), app->idf_ver); + return 0; +} + +static int cmd_uptime(int argc, char **argv) +{ + (void)argc; + (void)argv; + printf("%lu s\n", (unsigned long)(xTaskGetTickCount() / + configTICK_RATE_HZ)); + return 0; +} + +static void commands_register(void) +{ + static const esp_console_cmd_t cmds[] = { + { .command = "free", .help = "Free heap", .func = cmd_free }, + { .command = "version", .help = "Firmware and library versions", + .func = cmd_version }, + { .command = "uptime", .help = "Seconds since the start", + .func = cmd_uptime }, + }; + esp_console_config_t cfg = ESP_CONSOLE_CONFIG_DEFAULT(); + + ESP_ERROR_CHECK(esp_console_init(&cfg)); + for (size_t i = 0; i < sizeof(cmds) / sizeof(cmds[0]); i++) { + ESP_ERROR_CHECK(esp_console_cmd_register(&cmds[i])); + } +} +#endif + +void app_main(void) +{ + esp_err_t e = nvs_flash_init(); + + if (e == ESP_ERR_NVS_NO_FREE_PAGES || + e == ESP_ERR_NVS_NEW_VERSION_FOUND) { + /* A partition this firmware cannot read: start it afresh. */ + ESP_ERROR_CHECK(nvs_flash_erase()); + e = nvs_flash_init(); + } + ESP_ERROR_CHECK(e); + ESP_ERROR_CHECK(esp_netif_init()); + ESP_ERROR_CHECK(esp_event_loop_create_default()); + wifi_start(); + +#if CONFIG_TEDGE_SHELL_COMMAND + commands_register(); +#endif + ESP_LOGI(TAG, "tiny-edge %s", tedge_version()); + if (tedge_init(NULL, &hooks) != 0 || tedge_start() != 0) { + ESP_LOGE(TAG, "the client did not start"); + return; + } + + for (;;) { + struct tedge_measurement_value v = { + .series = "uptime", + .value = (double)xTaskGetTickCount() / + configTICK_RATE_HZ, + .unit = "s", + }; + + vTaskDelay(pdMS_TO_TICKS(CONFIG_SAMPLE_MEASUREMENT_INTERVAL_S * + 1000)); + (void)tedge_publish_measurement("sample", &v, 1, 0); + } +} diff --git a/ports/freertos/esp-idf/samples/minimal/partitions.csv b/ports/freertos/esp-idf/samples/minimal/partitions.csv new file mode 100644 index 0000000..a57f8b5 --- /dev/null +++ b/ports/freertos/esp-idf/samples/minimal/partitions.csv @@ -0,0 +1,7 @@ +# Two OTA slots for firmware update, sized for a 4 MB flash. +# Name, Type, SubType, Offset, Size +nvs, data, nvs, 0x9000, 0x6000 +otadata, data, ota, 0xf000, 0x2000 +phy_init, data, phy, 0x11000, 0x1000 +ota_0, app, ota_0, 0x20000, 0x1c0000 +ota_1, app, ota_1, 0x1e0000, 0x1c0000 diff --git a/ports/freertos/esp-idf/samples/minimal/sdkconfig.ci.small b/ports/freertos/esp-idf/samples/minimal/sdkconfig.ci.small new file mode 100644 index 0000000..0cff39d --- /dev/null +++ b/ports/freertos/esp-idf/samples/minimal/sdkconfig.ci.small @@ -0,0 +1,11 @@ +# CI: the other authentication, and most features off, so what the core +# leaves out (tedge_absent.c) must still link. +CONFIG_TEDGE_AUTH_BOOTSTRAP=y +CONFIG_TEDGE_C8Y_CORE_MQTT=y +CONFIG_TEDGE_HEALTH=n +CONFIG_TEDGE_RESTART=n +CONFIG_TEDGE_FIRMWARE_UPDATE=n +CONFIG_TEDGE_LOG_UPLOAD=n +CONFIG_TEDGE_PARAMETERS=n +CONFIG_TEDGE_C8Y_OPERATION_JSON=n +CONFIG_TEDGE_SHELL_COMMAND=n diff --git a/ports/freertos/esp-idf/samples/minimal/sdkconfig.defaults b/ports/freertos/esp-idf/samples/minimal/sdkconfig.defaults new file mode 100644 index 0000000..57dfde8 --- /dev/null +++ b/ports/freertos/esp-idf/samples/minimal/sdkconfig.defaults @@ -0,0 +1,14 @@ +# Firmware update: two OTA slots and the bootloader's rollback. +CONFIG_PARTITION_TABLE_CUSTOM=y +CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions.csv" +CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y +CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE=y + +# The firmware confirmation deadline runs on the timer task. +CONFIG_FREERTOS_TIMER_TASK_STACK_DEPTH=3072 + +# Plain lines in the uploaded log. +CONFIG_LOG_COLORS=n + +# The cloud may run the sample's own commands (main.c), and nothing else. +CONFIG_TEDGE_SHELL_COMMAND_ALLOW_LIST="free,version,uptime" diff --git a/ports/freertos/esp-idf/tiny_edge/CMakeLists.txt b/ports/freertos/esp-idf/tiny_edge/CMakeLists.txt new file mode 100644 index 0000000..c3c9878 --- /dev/null +++ b/ports/freertos/esp-idf/tiny_edge/CMakeLists.txt @@ -0,0 +1,78 @@ +# SPDX-License-Identifier: Apache-2.0 +# +# The FreeRTOS port of tiny-edge on ESP-IDF, as an ESP-IDF component: the +# thin-edge.io client for ESP32 applications. Add it to a project with the +# component manager (idf_component.yml): +# +# dependencies: +# tiny_edge: +# git: https://github.com/thin-edge/tiny-edge.git +# path: ports/freertos/esp-idf/tiny_edge +# +# and REQUIRES tiny_edge in the application's component. The options are in +# Kconfig (idf.py menuconfig, "tiny-edge (thin-edge.io client)"). +# +# The sources come from four places: the portable core (core/), the +# FreeRTOS kernel layer every FreeRTOS platform shares (ports/freertos/), +# the BSD-sockets and mbedTLS code shared with the POSIX port +# (ports/common/), and ESP-IDF's own (src/ here). + +get_filename_component(TEDGE_ROOT ${CMAKE_CURRENT_LIST_DIR}/../../../.. ABSOLUTE) +set(TEDGE_FREERTOS_DIR ${TEDGE_ROOT}/ports/freertos) +set(TEDGE_COMMON_DIR ${TEDGE_ROOT}/ports/common) + +include(${TEDGE_ROOT}/core/sources.cmake) +tedge_core_sources(_core_sources CONFIG_) + +set(_srcs + ${_core_sources} + ${TEDGE_FREERTOS_DIR}/src/tedge_port_os.c + ${TEDGE_COMMON_DIR}/src/tedge_sockets.c + src/tedge_port_device.c + src/tedge_port_log.c + src/tedge_port_kv.c + src/tedge_port_net.c + src/tedge_port_time.c +) +if(CONFIG_TEDGE_AUTH_C8Y_CA) + list(APPEND _srcs + ${TEDGE_COMMON_DIR}/src/tedge_mbedtls_key.c + src/tedge_port_crypto.c + ) +endif() +if(CONFIG_TEDGE_FIRMWARE_UPDATE) + list(APPEND _srcs src/tedge_port_fw.c) +endif() +if(CONFIG_TEDGE_SHELL_COMMAND) + list(APPEND _srcs src/tedge_shell_cmd.c) +endif() + +idf_component_register( + SRCS ${_srcs} + # The public API (tedge/tedge.h) and the port's headers the core includes + # (, ). + INCLUDE_DIRS ${TEDGE_ROOT}/include ${TEDGE_FREERTOS_DIR}/include include + PRIV_INCLUDE_DIRS ${TEDGE_ROOT}/core/src ${TEDGE_COMMON_DIR}/include + REQUIRES freertos log mbedtls + PRIV_REQUIRES esp_netif esp_event esp_wifi esp_app_format app_update + esp_hw_support esp_system nvs_flash lwip console + # The log capture registers itself in a constructor, which nothing + # references by name. + WHOLE_ARCHIVE +) + +file(STRINGS ${TEDGE_ROOT}/VERSION TEDGE_VERSION LIMIT_COUNT 1) +target_compile_definitions(${COMPONENT_LIB} PRIVATE + TEDGE_VERSION_STRING="${TEDGE_VERSION}" + # lwIP's resolver reports a timeout and an unknown name alike. + TEDGE_COMMON_LWIP=1 +) +target_compile_options(${COMPONENT_LIB} PRIVATE -Wno-unused-parameter) + +# from this component's Kconfig, and the trust anchors. +set(_generated ${CMAKE_CURRENT_BINARY_DIR}/generated) +include(${CMAKE_CURRENT_LIST_DIR}/cmake/tedge_config.cmake) +tedge_generate_config(${CMAKE_CURRENT_LIST_DIR}/Kconfig ${_generated}) +include(${TEDGE_COMMON_DIR}/cmake/tedge_ca_bundle.cmake) +tedge_ca_bundle(${_generated} "${CONFIG_TEDGE_C8Y_CA_FILES}") +target_include_directories(${COMPONENT_LIB} PUBLIC ${_generated}) diff --git a/ports/freertos/esp-idf/tiny_edge/Kconfig b/ports/freertos/esp-idf/tiny_edge/Kconfig new file mode 100644 index 0000000..f904541 --- /dev/null +++ b/ports/freertos/esp-idf/tiny_edge/Kconfig @@ -0,0 +1,431 @@ +# SPDX-License-Identifier: Apache-2.0 +# +# The client's options on ESP-IDF. The names are the core's (TEDGE_*), so +# the core sees CONFIG_TEDGE_HEALTH as TEDGE_HEALTH (cmake/tedge_config.cmake +# maps them), and the defaults are the other ports' unless ESP-IDF needs +# something else. Set them with `idf.py menuconfig` or sdkconfig.defaults. + +menu "tiny-edge (thin-edge.io client)" + +config TEDGE_TRANSPORT_C8Y + bool + default y + # Several client threads hold TLS sessions at once, and share PSA's + # key slots and random generator. + select MBEDTLS_THREADING_C + +menu "Cumulocity" + +config TEDGE_C8Y_URL + string "Cumulocity tenant host" + default "" + help + Host name of the Cumulocity tenant, e.g. "example.cumulocity.com", + without a scheme. The application can also set it at run time + (tedge_set_c8y_url()), which the client remembers. + +config TEDGE_C8Y_CA_FILES + string "Trust anchors for the tenant (PEM files, space-separated)" + default "certs/godaddy-root-g2.pem" + help + Paths relative to the tiny-edge repository, or absolute. They are + embedded and given to TLS as the server's trust anchors. The default + is the root the *.cumulocity.com chains end in. + +config TEDGE_SNTP + bool "Set the clock with SNTP when it is not already valid" + default y + help + Certificates cannot be validated without the time. With this off, + the application must set the clock before the client connects. + +config TEDGE_SNTP_SERVER + string "SNTP server" + default "pool.ntp.org" + depends on TEDGE_SNTP + +config TEDGE_C8Y_OPERATION_JSON + bool "Receive operations with their id (devicecontrol/notifications)" + default y + +config TEDGE_C8Y_PAYLOAD_BYTES + int "Largest message the client accepts" + default 2048 if TEDGE_C8Y_OPERATION_JSON + default 1024 + range 512 8192 + +config TEDGE_REQUIRED_INTERVAL_MIN + int "Required interval reported to Cumulocity (minutes)" + default 60 + +config TEDGE_RECONNECT_BACKOFF_MAX_S + int "Longest wait between reconnect attempts (s)" + default 300 + range 5 3600 + +choice TEDGE_AUTH + prompt "How the device authenticates" + default TEDGE_AUTH_C8Y_CA + +config TEDGE_AUTH_C8Y_CA + bool "Certificate from the Cumulocity CA (enrollment)" + # The device writes its own certificate request. + select MBEDTLS_X509_CREATE_C + select MBEDTLS_X509_CSR_WRITE_C + select MBEDTLS_PK_WRITE_C + select MBEDTLS_PEM_WRITE_C + +config TEDGE_AUTH_BOOTSTRAP + bool "Device credentials from a bootstrap user" + +endchoice + +config TEDGE_ENROLL_POLL_S + int "Seconds between enrollment attempts" + default 10 + depends on TEDGE_AUTH_C8Y_CA + +config TEDGE_BOOTSTRAP_USER + string "Bootstrap user (tenant/username)" + default "" + depends on TEDGE_AUTH_BOOTSTRAP + +config TEDGE_BOOTSTRAP_PASSWORD + string "Bootstrap password" + default "" + depends on TEDGE_AUTH_BOOTSTRAP + +choice TEDGE_C8Y_ENDPOINT + prompt "Cumulocity MQTT endpoint" + default TEDGE_C8Y_MQTT_SERVICE if TEDGE_AUTH_C8Y_CA + default TEDGE_C8Y_CORE_MQTT + +config TEDGE_C8Y_MQTT_SERVICE + bool "MQTT service (free-form topics)" + depends on TEDGE_AUTH_C8Y_CA + +config TEDGE_C8Y_CORE_MQTT + bool "Core MQTT (SmartREST)" + +endchoice + +config TEDGE_DEVICE_ID_PREFIX + string "Default external ID prefix" + default "tedge" + help + The default device id is this prefix followed by the station MAC + address. + +config TEDGE_DEVICE_TYPE + string "Default device type" + default "thin-edge.io-esp-idf" + +endmenu # Cumulocity + +menu "Features" + +config TEDGE_TELEMETRY + bool "Telemetry (measurements, events, alarms)" + default y + +config TEDGE_TELEMETRY_BUFFER_BYTES + int "Bytes held for telemetry that cannot be sent yet" + default 2048 + depends on TEDGE_TELEMETRY + +config TEDGE_HEALTH + bool "Device health measurements" + default y + select TEDGE_TELEMETRY + +config TEDGE_HEALTH_INTERVAL_S + int "Seconds between health measurements" + default 900 + depends on TEDGE_HEALTH + +config TEDGE_RESTART + bool "Restart operation" + default y + +config TEDGE_FIRMWARE_UPDATE + bool "Firmware update (OTA partitions, with rollback)" + default y + select TEDGE_HTTP + help + Writes the new image to the next OTA partition and boots it on + trial: the image is kept only once it confirms itself, and the + bootloader rolls it back otherwise. Needs a partition table with two + OTA app partitions and CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE. + +if TEDGE_FIRMWARE_UPDATE + +config TEDGE_FIRMWARE_STACK_SIZE + int "Stack of the download thread (bytes)" + default 12288 + help + A TLS handshake runs on this thread. ESP-IDF's mbedTLS needs more + stack for it than Zephyr's: the log upload thread was measured at + 8.1 KB on an ESP32-C6, so 8192 overflows. 12288 leaves a 4 KB margin. + +config TEDGE_FIRMWARE_CONFIRM_AFTER_CONNECT + bool "Confirm a test-booted image once it reaches the cloud" + default y + +config TEDGE_FIRMWARE_CONFIRM_TIMEOUT_S + int "Seconds a test-booted image has to confirm itself" + default 900 + range 0 86400 + +config TEDGE_FIRMWARE_PROGRESS + bool "Publish download progress on a free-form topic" + default y + depends on TEDGE_C8Y_MQTT_SERVICE + +config TEDGE_FIRMWARE_PROGRESS_PERCENT + int "Percentage step between progress messages" + default 10 + range 1 50 + depends on TEDGE_FIRMWARE_PROGRESS + +endif # TEDGE_FIRMWARE_UPDATE + +config TEDGE_SHELL_COMMAND + bool "Shell command operation (c8y_Command)" + default y + help + Runs an allow-listed ESP-IDF console command (esp_console) sent + from the cloud, and returns what it printed. The command runs in + the client, not in a shell on the network. The application + registers its commands with esp_console_cmd_register(), and nothing + runs until TEDGE_SHELL_COMMAND_ALLOW_LIST says what may. + +if TEDGE_SHELL_COMMAND + +config TEDGE_SHELL_COMMAND_ALLOW_LIST + string "Commands the cloud may run (comma-separated prefixes)" + default "" + help + For example "free,version". A request must begin with one of + these and may add arguments; anything that could chain a second + command is refused whatever this says. + + Empty, the default, refuses every command: compiling the feature in + is not the same as consenting to run something. What is safe to + expose depends on the commands the image registers, which only you + know. + +config TEDGE_SHELL_COMMAND_TIMEOUT_S + int "Seconds before a command is reported as timed out" + default 30 + range 1 600 + help + A running console command cannot be interrupted, so the operation + fails but the thread stays busy until the command returns. Keep + commands that can block for ever off the allow-list. + +config TEDGE_SHELL_COMMAND_OUTPUT_BYTES + int "Output kept from a command (bytes)" + default 512 + range 64 4096 + help + What the command printed, as far as this many bytes. The result + reported to Cumulocity is one SmartREST field and is shorter still; + a command with a lot to say belongs behind a log type. + +config TEDGE_SHELL_COMMAND_STACK_SIZE + int "Stack of the thread a command runs on (bytes)" + default 4096 + help + The command runs on this thread. The sample's commands, which + print with printf, used 1.2 KB of it on an ESP32-C6; a command that + needs more stack needs a larger value. + +endif # TEDGE_SHELL_COMMAND + +config TEDGE_LOG_UPLOAD + bool "Log upload" + default y + select TEDGE_HTTP + help + Keeps the image's recent log lines (ESP_LOG output) in RAM and + uploads them when the cloud asks. + +if TEDGE_LOG_UPLOAD + +config TEDGE_LOG_RING_BYTES + int "The image's log kept in RAM (bytes)" + default 2048 + +config TEDGE_LOG_TYPE_SLOTS + int "Log types the client can hold" + default 4 + +config TEDGE_LOG_UPLOAD_MAX_BYTES + int "Most bytes one request may send" + default 8192 + +config TEDGE_LOG_UPLOAD_STACK_SIZE + int "Stack of the upload thread (bytes)" + default 12288 + help + A TLS handshake runs on this thread. ESP-IDF's mbedTLS needs more + stack for it than Zephyr's: the log upload thread was measured at + 8.1 KB on an ESP32-C6, so 8192 overflows. 12288 leaves a 4 KB margin. + +endif # TEDGE_LOG_UPLOAD + +config TEDGE_REMOTE_ACCESS + bool "Remote access (tunnel to the device and its network)" + default y + depends on TEDGE_AUTH_C8Y_CA + select TEDGE_HTTP_CLIENT + +if TEDGE_REMOTE_ACCESS + +choice TEDGE_REMOTE_ACCESS_TARGETS + prompt "Which hosts a tunnel may reach" + default TEDGE_REMOTE_ACCESS_TARGETS_LAN + +config TEDGE_REMOTE_ACCESS_TARGETS_LAN + bool "The device and its subnet" + +config TEDGE_REMOTE_ACCESS_TARGETS_LIST + bool "Only the allow-list" + +config TEDGE_REMOTE_ACCESS_TARGETS_LOCAL + bool "Only the device itself" + +endchoice + +config TEDGE_REMOTE_ACCESS_ALLOW_LIST + string "Allow-list of host:port targets" + default "" + +config TEDGE_REMOTE_ACCESS_STACK_SIZE + int "Stack of each session's bridge thread (bytes)" + default 12288 + help + A TLS handshake runs on this thread. ESP-IDF's mbedTLS needs more + stack for it than Zephyr's: the log upload thread was measured at + 8.1 KB on an ESP32-C6, so 8192 overflows. 12288 leaves a 4 KB margin. + +config TEDGE_REMOTE_ACCESS_TWIN_SESSIONS + bool "List the open sessions' targets in the twin data" + default y + +config TEDGE_REMOTE_ACCESS_MAX_SESSIONS + int "Maximum concurrent sessions" + default 1 + +config TEDGE_REMOTE_ACCESS_BUF_SIZE + int "Bridge buffer size per direction (bytes)" + default 2048 + +config TEDGE_REMOTE_ACCESS_IDLE_TIMEOUT_S + int "Close a session after this many idle seconds (0 = never)" + default 900 + +config TEDGE_REMOTE_ACCESS_MDNS + bool "Resolve .local targets over mDNS" + default y + +config TEDGE_REMOTE_ACCESS_RESOLVE_TIMEOUT_MS + int "How long each lookup of a target name waits (ms)" + default 5000 + +config TEDGE_REMOTE_ACCESS_RESOLVE_ATTEMPTS + int "Lookups of a target name before the session fails" + default 3 + +config TEDGE_REMOTE_ACCESS_RESOLVE_RETRY_DELAY_MS + int "Pause between lookups of a target name (ms)" + default 500 + +endif # TEDGE_REMOTE_ACCESS + +config TEDGE_PARAMETERS + bool "Parameters" + default y + +config TEDGE_PARAMETERS_MAX + int "Maximum declared parameters, across all sets" + default 16 + depends on TEDGE_PARAMETERS + +config TEDGE_PARAMETERS_SELF + bool "Offer the client's own tunables as a parameter set" + default y + depends on TEDGE_PARAMETERS + +config TEDGE_CERT_RENEWAL + bool "Certificate renewal" + default y + depends on TEDGE_AUTH_C8Y_CA + select TEDGE_HTTP + +if TEDGE_CERT_RENEWAL + +config TEDGE_CERT_RENEW_BEFORE_DAYS + int "Renew when this many days of lifetime are left" + default 30 + +config TEDGE_CERT_RENEW_ALARM_DAYS + int "Raise an alarm when this many days are left and renewal failed" + default 7 + +config TEDGE_CERT_RENEW_CHECK_HOURS + int "Hours between certificate checks" + default 24 + +endif # TEDGE_CERT_RENEWAL + +endmenu # Features + +# What the features need: never set directly. +config TEDGE_HTTP_CLIENT + bool + default y if TEDGE_AUTH_C8Y_CA + +config TEDGE_HTTP + bool + select TEDGE_HTTP_CLIENT + +config TEDGE_HTTP_MAX_REDIRECTS + int "HTTP redirects to follow" + default 3 + depends on TEDGE_HTTP + +menu "Resources" + +config TEDGE_LOG_LEVEL + int "Log level (0 none, 1 error, 2 warning, 3 info, 4 debug)" + default 3 + range 0 4 + +config TEDGE_THREAD_STACK_SIZE + int "Client thread stack size (bytes)" + default 8192 + help + The client thread runs the TLS handshakes with the tenant, which + take more stack with ESP-IDF's mbedTLS than with Zephyr's. + +config TEDGE_THREAD_PRIORITY + int "Client thread priority (FreeRTOS priority)" + default 5 + range 1 24 + help + Every client thread runs at this priority. Keep it below the + application's time-critical tasks and the Wi-Fi and lwIP tasks + (ESP-IDF runs those at 18 to 23). + +config TEDGE_HEAP_SIZE + int "Client heap allowance (bytes)" + default 16384 if TEDGE_PARAMETERS + default 10240 + help + The most the client takes from the heap at once, so its use is + bounded and measured. TLS buffers are mbedTLS's, outside of it. + +endmenu # Resources + +endmenu diff --git a/ports/freertos/esp-idf/tiny_edge/cmake/tedge_config.cmake b/ports/freertos/esp-idf/tiny_edge/cmake/tedge_config.cmake new file mode 100644 index 0000000..9db0651 --- /dev/null +++ b/ports/freertos/esp-idf/tiny_edge/cmake/tedge_config.cmake @@ -0,0 +1,46 @@ +# SPDX-License-Identifier: Apache-2.0 +# +# The core reads its build configuration as TEDGE_* macros from +# and never sees a Kconfig symbol. On ESP-IDF that header +# is generated here, as the Zephyr port does it: one entry for each option +# this component's Kconfig declares, +# +# #if defined(CONFIG_TEDGE_HEALTH) +# #define TEDGE_HEALTH CONFIG_TEDGE_HEALTH +# #endif +# +# so the values still come from sdkconfig.h, and an option added to the +# Kconfig reaches the core without anyone editing a mapping. + +function(tedge_generate_config kconfig out_dir) + file(STRINGS ${kconfig} _lines REGEX "^[ \t]*(menu)?config[ \t]+TEDGE") + set(_names "") + foreach(_line ${_lines}) + string(REGEX REPLACE "^[ \t]*(menu)?config[ \t]+(TEDGE[A-Z0-9_]*).*" "\\2" + _name "${_line}") + list(APPEND _names ${_name}) + endforeach() + list(REMOVE_DUPLICATES _names) + list(SORT _names) + + set(_body "") + foreach(_name ${_names}) + string(APPEND _body + "#if defined(CONFIG_${_name})\n#define ${_name} CONFIG_${_name}\n#endif\n") + endforeach() + + file(CONFIGURE OUTPUT ${out_dir}/tedge_config.h CONTENT +"/* Generated by ports/freertos/esp-idf/tiny_edge/cmake/tedge_config.cmake. Do not edit. */ +#ifndef TEDGE_CONFIG_H_ +#define TEDGE_CONFIG_H_ + +#include + +/* How a user of this port names an option, for messages that tell them + * which one to change. */ +#define TEDGE_CONFIG_NAME(name) \"CONFIG_\" #name + +${_body} +#endif /* TEDGE_CONFIG_H_ */ +" @ONLY) +endfunction() diff --git a/ports/freertos/esp-idf/tiny_edge/idf_component.yml b/ports/freertos/esp-idf/tiny_edge/idf_component.yml new file mode 100644 index 0000000..320df8d --- /dev/null +++ b/ports/freertos/esp-idf/tiny_edge/idf_component.yml @@ -0,0 +1,5 @@ +description: "tiny-edge: the thin-edge.io client for Cumulocity, on ESP-IDF (FreeRTOS)" +url: "https://github.com/thin-edge/tiny-edge" +license: "Apache-2.0" +dependencies: + idf: ">=6.1" diff --git a/ports/freertos/esp-idf/tiny_edge/include/tedge_port/log.h b/ports/freertos/esp-idf/tiny_edge/include/tedge_port/log.h new file mode 100644 index 0000000..9a75e8b --- /dev/null +++ b/ports/freertos/esp-idf/tiny_edge/include/tedge_port/log.h @@ -0,0 +1,34 @@ +/* SPDX-License-Identifier: Apache-2.0 */ + +/* ESP-IDF implementation of the logging macros in : the + * client logs with ESP_LOG under the tag "tedge", so `esp_log_level_set()` + * and the console treat its lines as the application's. A line below + * CONFIG_TEDGE_LOG_LEVEL is compiled out. */ + +#ifndef TEDGE_PORT_ESP_IDF_LOG_H_ +#define TEDGE_PORT_ESP_IDF_LOG_H_ + +#include + +/* The tag, defined once by TEDGE_LOG_REGISTER() (in tedge_core.c). */ +extern const char tedge_log_tag[]; + +#define TEDGE_LOG_REGISTER() const char tedge_log_tag[] = "tedge" +#define TEDGE_LOG_DECLARE() extern const char tedge_log_tag[] + +#define TEDGE_LOG_AT_(level, esp_macro, ...) \ + do { \ + if (TEDGE_LOG_LEVEL >= (level)) { \ + esp_macro(tedge_log_tag, __VA_ARGS__); \ + } \ + } while (0) + +#define TEDGE_LOG_ERR(...) TEDGE_LOG_AT_(1, ESP_LOGE, __VA_ARGS__) +#define TEDGE_LOG_WRN(...) TEDGE_LOG_AT_(2, ESP_LOGW, __VA_ARGS__) +#define TEDGE_LOG_INF(...) TEDGE_LOG_AT_(3, ESP_LOGI, __VA_ARGS__) +#define TEDGE_LOG_DBG(...) TEDGE_LOG_AT_(4, ESP_LOGD, __VA_ARGS__) + +/* esp_log_level_set() changes a tag's level while the image runs. */ +#define TEDGE_PORT_LOG_RUNTIME_LEVEL 1 + +#endif /* TEDGE_PORT_ESP_IDF_LOG_H_ */ diff --git a/ports/freertos/esp-idf/tiny_edge/src/tedge_port_crypto.c b/ports/freertos/esp-idf/tiny_edge/src/tedge_port_crypto.c new file mode 100644 index 0000000..2af59f2 --- /dev/null +++ b/ports/freertos/esp-idf/tiny_edge/src/tedge_port_crypto.c @@ -0,0 +1,31 @@ +/* SPDX-License-Identifier: Apache-2.0 + * + * Where the ESP-IDF port keeps the device key: NVS, through the client's + * own store (tedge_port_kv.c), as PEM. Generating it and signing the CSR + * are shared with other ports (ports/common/src/tedge_mbedtls_key.c). + * + * NVS is plain flash unless the application turns on NVS encryption + * (CONFIG_NVS_ENCRYPTION, with flash encryption), which a production + * device should: the key is only as safe as the flash it is in. + */ + +#include + +#include "tedge_common.h" + +#define KEY "tedge/device-key" + +const char *tedge_common_key_where(void) +{ + return "NVS " KEY; +} + +int tedge_common_key_load(char *pem, size_t cap) +{ + return tedge_port_kv_get(KEY, pem, cap); +} + +int tedge_common_key_store(const char *pem, size_t len) +{ + return tedge_port_kv_set(KEY, pem, len); +} diff --git a/ports/freertos/esp-idf/tiny_edge/src/tedge_port_device.c b/ports/freertos/esp-idf/tiny_edge/src/tedge_port_device.c new file mode 100644 index 0000000..84cc575 --- /dev/null +++ b/ports/freertos/esp-idf/tiny_edge/src/tedge_port_device.c @@ -0,0 +1,85 @@ +/* SPDX-License-Identifier: Apache-2.0 + * + * The ESP-IDF side of , and of the random numbers in + * and . + */ + +#include +#include +#include + +#include +#include +#include +#include +#include + +#include + +/* --- The device ---------------------------------------------------------- */ + +/* esp_reset_reason()'s value (ESP_RST_POWERON, ESP_RST_SW, ESP_RST_PANIC, + * ...), as a number. It is the reason for this boot; ESP-IDF works it out + * afresh on every boot, so there is nothing to clear. */ +int tedge_port_reset_cause(uint32_t *cause) +{ + *cause = (uint32_t)esp_reset_reason(); + return 0; +} + +/* The MAC of the default interface (the Wi-Fi station's, as a rule), or + * the station MAC the chip was given in the factory when no interface + * exists yet: the same on every boot either way. */ +int tedge_port_mac_address(uint8_t mac[6]) +{ + esp_netif_t *netif = esp_netif_get_default_netif(); + + if (netif != NULL && esp_netif_get_mac(netif, mac) == ESP_OK) { + return 0; + } + return (esp_read_mac(mac, ESP_MAC_WIFI_STA) == ESP_OK) ? 0 : -ENODEV; +} + +/* The project's name and version, as the build put them in the image's + * description (PROJECT_VER, or `git describe`). */ +const char *tedge_port_firmware_name(void) +{ + return esp_app_get_description()->project_name; +} + +const char *tedge_port_firmware_version(void) +{ + return esp_app_get_description()->version; +} + +/* esp_restart() stops Wi-Fi and the other radios before a full-system + * reset, which is what the interface asks for. */ +_Noreturn void tedge_port_reset(void) +{ + esp_restart(); +} + +/* Crash dumps (TEDGE_COREDUMP) are not in this port yet: ESP-IDF's core + * dump partition would be offered as a log type. */ +void tedge_port_coredump_init(void) +{ +} + +void tedge_port_coredump_taken(void) +{ +} + +/* --- Random numbers ------------------------------------------------------ */ + +/* The hardware generator: cryptographically strong while a radio runs, + * and also before, since the bootloader seeds it from its entropy source. */ +uint32_t tedge_port_random32(void) +{ + return esp_random(); +} + +int tedge_port_random(void *buf, size_t len) +{ + esp_fill_random(buf, len); + return 0; +} diff --git a/ports/freertos/esp-idf/tiny_edge/src/tedge_port_fw.c b/ports/freertos/esp-idf/tiny_edge/src/tedge_port_fw.c new file mode 100644 index 0000000..0318513 --- /dev/null +++ b/ports/freertos/esp-idf/tiny_edge/src/tedge_port_fw.c @@ -0,0 +1,142 @@ +/* SPDX-License-Identifier: Apache-2.0 + * + * The ESP-IDF side of : OTA app partitions, with the + * bootloader's rollback (CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE), which is + * the MCUboot model the interface describes. A new image is written to the + * next OTA partition and made the boot partition; it then boots in the + * "pending verify" state, and the bootloader returns to the old image if + * it resets before confirming itself. + */ + +#include +#include + +#include +#include + +#include +#include + +#if !defined(CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE) +#error "TEDGE_FIRMWARE_UPDATE needs CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE" +#endif + +TEDGE_LOG_DECLARE(); + +static esp_ota_handle_t ota; +static const esp_partition_t *target; + +static int errno_of(esp_err_t err) +{ + switch (err) { + case ESP_OK: + return 0; + case ESP_ERR_NO_MEM: + return -ENOMEM; + case ESP_ERR_NOT_FOUND: + return -ENOENT; + case ESP_ERR_INVALID_SIZE: + case ESP_ERR_OTA_PARTITION_CONFLICT: + return -ENOSPC; + case ESP_ERR_OTA_VALIDATE_FAILED: + case ESP_ERR_INVALID_ARG: + return -EINVAL; + default: + return -EIO; + } +} + +int tedge_port_fw_begin(void) +{ + esp_err_t e; + + if (ota != 0) { + (void)esp_ota_abort(ota); /* a download that never finished */ + ota = 0; + } + target = esp_ota_get_next_update_partition(NULL); + if (target == NULL) { + TEDGE_LOG_ERR("firmware: no OTA partition to write to (the " + "partition table needs two OTA app partitions)"); + return -ENOENT; + } + /* Erased as it is written, not all at once: a whole partition takes + * seconds to erase, and the download would stall meanwhile. */ + e = esp_ota_begin(target, OTA_WITH_SEQUENTIAL_WRITES, &ota); + if (e != ESP_OK) { + TEDGE_LOG_ERR("firmware: esp_ota_begin(%s): %s", target->label, + esp_err_to_name(e)); + ota = 0; + } + return errno_of(e); +} + +int tedge_port_fw_write(const void *data, size_t len) +{ + if (ota == 0) { + return -EINVAL; + } + return errno_of(esp_ota_write(ota, data, len)); +} + +int tedge_port_fw_finish(void) +{ + esp_err_t e; + + if (ota == 0) { + return -EINVAL; + } + /* Checks the image (its header, its hash, and its signature when + * secure boot is on). */ + e = esp_ota_end(ota); + ota = 0; + if (e != ESP_OK) { + TEDGE_LOG_ERR("firmware: the image is not valid (%s)", + esp_err_to_name(e)); + } + return errno_of(e); +} + +int tedge_port_fw_request_trial(void) +{ + esp_err_t e; + + if (target == NULL) { + return -EINVAL; + } + e = esp_ota_set_boot_partition(target); + if (e != ESP_OK) { + TEDGE_LOG_ERR("firmware: could not boot %s next (%s)", + target->label, esp_err_to_name(e)); + } + return errno_of(e); +} + +bool tedge_port_fw_is_confirmed(void) +{ + esp_ota_img_states_t state; + + if (esp_ota_get_state_partition(esp_ota_get_running_partition(), + &state) != ESP_OK) { + /* A factory partition has no state: nothing to confirm. */ + return true; + } + return state != ESP_OTA_IMG_PENDING_VERIFY; +} + +int tedge_port_fw_confirm(void) +{ + return errno_of(esp_ota_mark_app_valid_cancel_rollback()); +} + +/* The image's version as the build stated it (PROJECT_VER). */ +int tedge_port_fw_running_version(char *buf, size_t len) +{ + const esp_app_desc_t *desc = esp_app_get_description(); + + if (desc->version[0] == '\0') { + return -ENOENT; + } + snprintf(buf, len, "%s", desc->version); + return 0; +} diff --git a/ports/freertos/esp-idf/tiny_edge/src/tedge_port_kv.c b/ports/freertos/esp-idf/tiny_edge/src/tedge_port_kv.c new file mode 100644 index 0000000..b7ac56d --- /dev/null +++ b/ports/freertos/esp-idf/tiny_edge/src/tedge_port_kv.c @@ -0,0 +1,252 @@ +/* SPDX-License-Identifier: Apache-2.0 + * + * The ESP-IDF side of : NVS, in the namespace "tedge" of + * the default "nvs" partition. + * + * An NVS key has at most 15 characters, and the client's keys are paths + * ("tedge/param/modbus/unit_id"). So each key is stored under a name made + * from its hash, "t" and 8 hex digits, and the value is a blob holding the + * full key, a NUL, then the value: a read checks the key it finds, and the + * listing (tedge_port_kv_foreach) reads the keys back. When two keys share + * a hash, the second takes the next of PROBES names. + */ + +#include +#include +#include + +#include +#include + +#include +#include +#include +#include + +TEDGE_LOG_DECLARE(); + +#define NAMESPACE "tedge" +#define PROBES 4 + +static nvs_handle_t nvs; +/* Finding a free name and taking it is one step for writers. */ +TEDGE_PORT_MUTEX_DEFINE(write_lock); + +static int errno_of(esp_err_t err) +{ + switch (err) { + case ESP_OK: + return 0; + case ESP_ERR_NVS_NOT_FOUND: + return -ENOENT; + case ESP_ERR_NO_MEM: + return -ENOMEM; + case ESP_ERR_NVS_NOT_ENOUGH_SPACE: + return -ENOSPC; + case ESP_ERR_NVS_INVALID_LENGTH: + case ESP_ERR_NVS_VALUE_TOO_LONG: + return -EFBIG; + default: + return -EIO; + } +} + +/* FNV-1a over the key, and over the probe number. */ +static void nvs_name(const char *key, int probe, char name[NVS_KEY_NAME_MAX_SIZE]) +{ + uint32_t h = 2166136261u; + + for (const char *p = key; *p != '\0'; p++) { + h = (h ^ (uint8_t)*p) * 16777619u; + } + h = (h ^ (uint32_t)probe) * 16777619u; + snprintf(name, NVS_KEY_NAME_MAX_SIZE, "t%08lx", (unsigned long)h); +} + +/* Read the blob stored as @p name into a buffer of the right size, which + * the caller frees. @return the buffer, or NULL with *err set. */ +static char *blob_read(const char *name, size_t *len, int *err) +{ + char *buf; + esp_err_t e = nvs_get_blob(nvs, name, NULL, len); + + if (e != ESP_OK) { + *err = errno_of(e); + return NULL; + } + buf = malloc(*len + 1); + if (buf == NULL) { + *err = -ENOMEM; + return NULL; + } + e = nvs_get_blob(nvs, name, buf, len); + if (e != ESP_OK) { + free(buf); + *err = errno_of(e); + return NULL; + } + buf[*len] = '\0'; /* so a blob without its NUL still ends */ + return buf; +} + +/* The name @p key is stored under, or with @p for_write the first free + * name when it is not stored. @return 0; -ENOENT; -ENOSPC when every name + * it could take holds another key. */ +static int find(const char *key, bool for_write, char name[NVS_KEY_NAME_MAX_SIZE]) +{ + char candidate[NVS_KEY_NAME_MAX_SIZE]; + bool have_free = false; + + for (int probe = 0; probe < PROBES; probe++) { + size_t len; + int err = 0; + char *blob; + + nvs_name(key, probe, candidate); + blob = blob_read(candidate, &len, &err); + if (blob == NULL) { + if (err == -ENOENT && for_write && !have_free) { + memcpy(name, candidate, NVS_KEY_NAME_MAX_SIZE); + have_free = true; + } + if (err != -ENOENT) { + return err; + } + continue; + } + bool same = strcmp(blob, key) == 0; + + free(blob); + if (same) { + memcpy(name, candidate, NVS_KEY_NAME_MAX_SIZE); + return 0; + } + } + if (!for_write) { + return -ENOENT; + } + return have_free ? 0 : -ENOSPC; +} + +int tedge_port_kv_init(void) +{ + /* The application has usually started NVS already (Wi-Fi needs it); + * a second init is harmless. One that needs an erase is not the + * client's to do: it would take the application's data with it. */ + esp_err_t e = nvs_flash_init(); + + if (e != ESP_OK) { + TEDGE_LOG_ERR("NVS did not start (%s): the application must " + "initialise or erase it", esp_err_to_name(e)); + return -EIO; + } + e = nvs_open(NAMESPACE, NVS_READWRITE, &nvs); + if (e != ESP_OK) { + TEDGE_LOG_ERR("NVS namespace %s did not open (%s)", NAMESPACE, + esp_err_to_name(e)); + return errno_of(e); + } + return 0; +} + +int tedge_port_kv_set(const char *key, const void *value, size_t len) +{ + char name[NVS_KEY_NAME_MAX_SIZE]; + size_t klen = strlen(key) + 1; + char *blob = malloc(klen + len); + esp_err_t e; + int ret; + + if (blob == NULL) { + return -ENOMEM; + } + memcpy(blob, key, klen); + memcpy(blob + klen, value, len); + tedge_port_mutex_lock(&write_lock); + ret = find(key, true, name); + if (ret == 0) { + e = nvs_set_blob(nvs, name, blob, klen + len); + if (e == ESP_OK) { + e = nvs_commit(nvs); + } + ret = errno_of(e); + } + tedge_port_mutex_unlock(&write_lock); + free(blob); + return ret; +} + +int tedge_port_kv_get(const char *key, void *buf, size_t cap) +{ + char name[NVS_KEY_NAME_MAX_SIZE]; + size_t len, klen = strlen(key) + 1; + int ret = find(key, false, name); + char *blob; + + if (ret != 0) { + return ret; + } + blob = blob_read(name, &len, &ret); + if (blob == NULL) { + return ret; + } + len -= klen; + if (len > cap) { + ret = -ENOSPC; + } else { + memcpy(buf, blob + klen, len); + ret = (int)len; + } + free(blob); + return ret; +} + +int tedge_port_kv_delete(const char *key) +{ + char name[NVS_KEY_NAME_MAX_SIZE]; + int ret; + + tedge_port_mutex_lock(&write_lock); + ret = find(key, false, name); + if (ret == 0) { + esp_err_t e = nvs_erase_key(nvs, name); + + if (e == ESP_OK) { + e = nvs_commit(nvs); + } + ret = errno_of(e); + } else if (ret == -ENOENT) { + ret = 0; + } + tedge_port_mutex_unlock(&write_lock); + return ret; +} + +int tedge_port_kv_foreach(const char *prefix, + void (*cb)(const char *subkey, void *ctx), void *ctx) +{ + size_t plen = strlen(prefix); + nvs_iterator_t it = NULL; + esp_err_t e; + + e = nvs_entry_find_in_handle(nvs, NVS_TYPE_BLOB, &it); + while (e == ESP_OK) { + nvs_entry_info_t info; + size_t len; + int err; + char *blob; + + (void)nvs_entry_info(it, &info); + blob = blob_read(info.key, &len, &err); + if (blob != NULL) { + if (strncmp(blob, prefix, plen) == 0 && + blob[plen] == '/' && blob[plen + 1] != '\0') { + cb(blob + plen + 1, ctx); + } + free(blob); + } + e = nvs_entry_next(&it); + } + nvs_release_iterator(it); + return (e == ESP_ERR_NVS_NOT_FOUND) ? 0 : errno_of(e); +} diff --git a/ports/freertos/esp-idf/tiny_edge/src/tedge_port_log.c b/ports/freertos/esp-idf/tiny_edge/src/tedge_port_log.c new file mode 100644 index 0000000..ffc62ce --- /dev/null +++ b/ports/freertos/esp-idf/tiny_edge/src/tedge_port_log.c @@ -0,0 +1,89 @@ +/* SPDX-License-Identifier: Apache-2.0 + * + * The ESP-IDF side of : the client's level is its tag's + * level in ESP_LOG. With log upload, every line the image logs is also + * copied into the client's log ring (core/src/tedge_log_ring.c), as the + * console shows it, through ESP_LOG's output function. + */ + +#include +#include + +#include "tedge_internal.h" + +#include + +#include +#include +#include +#include + +TEDGE_LOG_DECLARE(); + +int tedge_port_log_set_level(enum tedge_log_level level) +{ + static const esp_log_level_t map[] = { + [TEDGE_LOG_LEVEL_NONE] = ESP_LOG_NONE, + [TEDGE_LOG_LEVEL_ERR] = ESP_LOG_ERROR, + [TEDGE_LOG_LEVEL_WRN] = ESP_LOG_WARN, + [TEDGE_LOG_LEVEL_INF] = ESP_LOG_INFO, + [TEDGE_LOG_LEVEL_DBG] = ESP_LOG_DEBUG, + }; + + if ((unsigned int)level >= sizeof(map) / sizeof(map[0])) { + return -EINVAL; + } + /* Lines below TEDGE_LOG_LEVEL are not in the image to be shown. */ + esp_log_level_set(tedge_log_tag, map[level]); + return 0; +} + +void tedge_port_log_flush(void) +{ + /* ESP_LOG writes each line as it is logged. */ +} + +#if defined(TEDGE_LOG_UPLOAD) + +/* The output function ESP_LOG had before: the console, as a rule. */ +static vprintf_like_t console; + +/* Called for each line, from whichever task logs (ESP_LOG hands over a + * whole line, its newline included). The buffer is on that task's stack, + * so it is kept small: a longer line is cut in the ring, never on the + * console, and the cut is marked and keeps the line's end, so the next + * line still starts on its own. */ +static int capture(const char *fmt, va_list args) +{ + static const char cut[] = "...\n"; + char buf[128]; + va_list copy; + size_t len; + int n; + + va_copy(copy, args); + n = vsnprintf(buf, sizeof(buf), fmt, copy); + va_end(copy); + if (n > 0) { + len = (size_t)n; + if (len >= sizeof(buf)) { + size_t f = strlen(fmt); + + len = sizeof(buf) - 1; + if (f > 0 && fmt[f - 1] == '\n') { + memcpy(buf + len - (sizeof(cut) - 1), cut, + sizeof(cut) - 1); + } + } + tedge_log_ring_write((const uint8_t *)buf, len); + } + return (console != NULL) ? console(fmt, args) : n; +} + +/* Before app_main(), so the ring holds the boot's lines too. */ +__attribute__((constructor)) static void capture_start(void) +{ + console = esp_log_set_vprintf(capture); +} + +#endif /* TEDGE_LOG_UPLOAD */ diff --git a/ports/freertos/esp-idf/tiny_edge/src/tedge_port_net.c b/ports/freertos/esp-idf/tiny_edge/src/tedge_port_net.c new file mode 100644 index 0000000..1bfa0c4 --- /dev/null +++ b/ports/freertos/esp-idf/tiny_edge/src/tedge_port_net.c @@ -0,0 +1,122 @@ +/* SPDX-License-Identifier: Apache-2.0 + * + * The ESP-IDF side of : the interface queries, from + * esp_netif and its IP events. The connections and TLS run over lwIP's + * BSD sockets with code shared with other ports + * (ports/common/src/tedge_sockets.c). + * + * The interface the client uses is the default one + * (esp_netif_get_default_netif()): the Wi-Fi station, or Ethernet, as the + * application set them up. + */ + +#include + +#include "tedge_common.h" + +#include +#include +#include + +#include + +static bool addr_of(esp_netif_t *netif, esp_netif_ip_info_t *info) +{ + return netif != NULL && esp_netif_is_netif_up(netif) && + esp_netif_get_ip_info(netif, info) == ESP_OK && + info->ip.addr != 0; +} + +bool tedge_port_net_is_up(void) +{ + esp_netif_ip_info_t info; + + return addr_of(esp_netif_get_default_netif(), &info); +} + +/* --- Watching ------------------------------------------------------------ */ + +static void (*net_cb)(bool up); + +/* Runs on the default event loop's task. An address that is lost is + * reported late by lwIP (IP_EVENT_STA_LOST_IP waits for its timer), so the + * station's disconnection counts as down at once. */ +static void on_event(void *arg, esp_event_base_t base, int32_t id, void *data) +{ + (void)arg; + (void)data; + if (base == IP_EVENT) { + switch (id) { + case IP_EVENT_STA_GOT_IP: + case IP_EVENT_ETH_GOT_IP: + case IP_EVENT_PPP_GOT_IP: + net_cb(true); + return; + case IP_EVENT_STA_LOST_IP: + case IP_EVENT_ETH_LOST_IP: + case IP_EVENT_PPP_LOST_IP: + net_cb(false); + return; + default: + return; + } + } + if (base == WIFI_EVENT && id == WIFI_EVENT_STA_DISCONNECTED) { + net_cb(false); + } +} + +/* The application creates the default event loop before it starts the + * network (esp_event_loop_create_default()), so it exists by the time the + * client starts. */ +void tedge_port_net_watch(void (*cb)(bool up)) +{ + net_cb = cb; + (void)esp_event_handler_register(IP_EVENT, ESP_EVENT_ANY_ID, on_event, + NULL); + (void)esp_event_handler_register(WIFI_EVENT, + WIFI_EVENT_STA_DISCONNECTED, on_event, + NULL); +} + +/* --- Addresses ----------------------------------------------------------- */ + +bool tedge_port_net_is_own(const uint8_t addr[4]) +{ + esp_netif_t *netif = NULL; + uint32_t a; + + if (addr[0] == 127) { + return true; /* the loopback */ + } + memcpy(&a, addr, 4); + /* Every interface, not only the default one. */ + while ((netif = esp_netif_next_unsafe(netif)) != NULL) { + esp_netif_ip_info_t info; + + if (addr_of(netif, &info) && info.ip.addr == a) { + return true; + } + } + return false; +} + +bool tedge_port_net_on_subnet(const uint8_t addr[4]) +{ + esp_netif_ip_info_t info; + uint32_t a; + + if (!addr_of(esp_netif_get_default_netif(), &info)) { + return false; + } + memcpy(&a, addr, 4); + return (a & info.netmask.addr) == (info.ip.addr & info.netmask.addr); +} + +/* --- Sockets ------------------------------------------------------------- */ + +/* lwIP sockets need nothing more: no exec, and no SIGPIPE. */ +void tedge_common_socket_opened(int fd) +{ + (void)fd; +} diff --git a/ports/freertos/esp-idf/tiny_edge/src/tedge_port_time.c b/ports/freertos/esp-idf/tiny_edge/src/tedge_port_time.c new file mode 100644 index 0000000..6c3ef54 --- /dev/null +++ b/ports/freertos/esp-idf/tiny_edge/src/tedge_port_time.c @@ -0,0 +1,83 @@ +/* SPDX-License-Identifier: Apache-2.0 + * + * The ESP-IDF side of : the system clock, set with + * ESP-IDF's SNTP client (esp_netif_sntp) when nobody else has set it. + */ + +#include +#include +#include + +#include +#include + +#include +#include + +TEDGE_LOG_DECLARE(); + +/* Anything before 2025-01-01 means "not set". */ +#define CLOCK_FLOOR 1735689600 + +/* How long one sync waits for an answer. */ +#define SYNC_TIMEOUT_MS 10000 + +bool tedge_port_time_is_valid(void) +{ + return time(NULL) > CLOCK_FLOOR; +} + +int tedge_port_time_sync(void) +{ +#if defined(TEDGE_SNTP) + static bool started; + esp_err_t e; + + if (tedge_port_time_is_valid()) { + return 0; + } + /* Started once and left running, so it keeps the clock right. When + * the application runs SNTP itself, the clock is valid by now, or + * its client is the one to wait for. */ + if (!started && !esp_sntp_enabled()) { + esp_sntp_config_t cfg = ESP_NETIF_SNTP_DEFAULT_CONFIG(TEDGE_SNTP_SERVER); + + e = esp_netif_sntp_init(&cfg); + if (e != ESP_OK) { + TEDGE_LOG_WRN("SNTP (%s) did not start (%s)", + TEDGE_SNTP_SERVER, esp_err_to_name(e)); + return -EIO; + } + started = true; + } + if (started) { + e = esp_netif_sntp_sync_wait(pdMS_TO_TICKS(SYNC_TIMEOUT_MS)); + if (e == ESP_OK || tedge_port_time_is_valid()) { + TEDGE_LOG_INF("clock set from %s", TEDGE_SNTP_SERVER); + return 0; + } + TEDGE_LOG_WRN("SNTP (%s): no answer yet", TEDGE_SNTP_SERVER); + return -ETIMEDOUT; + } + return tedge_port_time_is_valid() ? 0 : -EAGAIN; +#else + if (tedge_port_time_is_valid()) { + return 0; + } + TEDGE_LOG_WRN("the clock is not set and %s is off: the application " + "must set it before the client can connect", + TEDGE_CONFIG_NAME(TEDGE_SNTP)); + return -ENOTSUP; +#endif +} + +int tedge_port_time_now(time_t *secs) +{ + time_t now = time(NULL); + + if (now <= CLOCK_FLOOR) { + return -EAGAIN; + } + *secs = now; + return 0; +} diff --git a/ports/freertos/esp-idf/tiny_edge/src/tedge_shell_cmd.c b/ports/freertos/esp-idf/tiny_edge/src/tedge_shell_cmd.c new file mode 100644 index 0000000..37d8c6f --- /dev/null +++ b/ports/freertos/esp-idf/tiny_edge/src/tedge_shell_cmd.c @@ -0,0 +1,227 @@ +/* SPDX-License-Identifier: Apache-2.0 + * + * Running a command the cloud asked for (c8y_Command), with ESP-IDF's + * console: the command is one the application registered with + * esp_console_cmd_register(), run with esp_console_run(), and what it + * prints is the operation's result. + * + * It runs in the client, not in a shell on the network, and on a thread of + * its own, so a command that takes a while does not hold the connection up. + * What may run is decided by core/src/tedge_shell_allow_list.c, which + * refuses everything until the integrator says otherwise. + * + * The output is captured through stdout. picolibc's stdout is one stream + * for every task, so while a command runs it is swapped for a stream that + * keeps what the command's task prints and passes everything else (other + * tasks' printf, and ESP_LOG, which prints through stdout) on to the + * console unchanged. + */ + +#include +#include +#include + +#include "tedge_internal.h" + +#include +#include +#include +#include + +#include +#include +#include + +TEDGE_LOG_DECLARE(); + +TEDGE_PORT_STACK_DEFINE(cmd_stack, TEDGE_SHELL_COMMAND_STACK_SIZE); +static tedge_port_thread_t cmd_thread; +TEDGE_PORT_MSGQ_DEFINE(cmd_events, sizeof(struct tedge_shell_event), 2); + +static struct { + char cmd[160]; + int64_t started; + bool running; + bool reported; +} job; + +/* ------------------------------------------------------------------------ */ +/* Capturing stdout */ +/* ------------------------------------------------------------------------ */ + +static FILE *console_out; /* stdout as it was before the command */ +static TaskHandle_t capture_task; /* whose output is kept */ +static char captured[TEDGE_SHELL_COMMAND_OUTPUT_BYTES]; +static size_t captured_len; + +static int capture_put(char c, FILE *f) +{ + (void)f; + if (capture_task != NULL && + xTaskGetCurrentTaskHandle() == capture_task) { + if (captured_len < sizeof(captured) - 1) { + captured[captured_len++] = c; + } + return (unsigned char)c; + } + return fputc(c, console_out); +} + +static int capture_flush(FILE *f) +{ + (void)f; + return fflush(console_out); +} + +static FILE capture_stream = + FDEV_SETUP_STREAM(capture_put, NULL, capture_flush, _FDEV_SETUP_WRITE); + +/* ------------------------------------------------------------------------ */ +/* Running it */ +/* ------------------------------------------------------------------------ */ + +static void run_thread(void *arg) +{ + struct tedge_shell_event ev = { 0 }; + esp_err_t err; + int ret = 0; + + (void)arg; + captured_len = 0; + fflush(stdout); + console_out = stdout; + capture_task = xTaskGetCurrentTaskHandle(); + stdout = &capture_stream; + + err = esp_console_run(job.cmd, &ret); + + stdout = console_out; + capture_task = NULL; + captured[captured_len] = '\0'; + + memcpy(ev.output, captured, captured_len + 1); + (void)tedge_shell_clean_output(ev.output); + if (err == ESP_ERR_NOT_FOUND) { + ev.rc = -ENOENT; + snprintf(ev.output, sizeof(ev.output), + "the command is allowed but not registered in this " + "image"); + } else if (err != ESP_OK) { + ev.rc = -EINVAL; + snprintf(ev.output, sizeof(ev.output), + "the command could not run (%s)", esp_err_to_name(err)); + } else { + ev.rc = ret; + if (ret != 0 && ev.output[0] == '\0') { + snprintf(ev.output, sizeof(ev.output), + "the command failed (%d)", ret); + } + } + (void)tedge_port_msgq_put(&cmd_events, &ev); + job.running = false; +} + +/* The application may have set the console up already, to register its + * commands; esp_console_run() needs it either way. */ +static int console_ready(void) +{ + static bool ready; + esp_console_config_t cfg = ESP_CONSOLE_CONFIG_DEFAULT(); + esp_err_t err; + + if (ready) { + return 0; + } + err = esp_console_init(&cfg); + if (err != ESP_OK && err != ESP_ERR_INVALID_STATE) { + TEDGE_LOG_ERR("shell: the console did not start (%s)", + esp_err_to_name(err)); + return -EIO; + } + ready = true; + return 0; +} + +/* ------------------------------------------------------------------------ */ +/* The request */ +/* ------------------------------------------------------------------------ */ + +int tedge_shell_request(const char *line, char *reason, size_t rlen) +{ + char cmd[160]; + const char *why = ""; + + /* 511,, */ + if (tedge_sr_field(line, 2, cmd, sizeof(cmd)) <= 0) { + snprintf(reason, rlen, "malformed command"); + return -EINVAL; + } + if (job.running) { + snprintf(reason, rlen, "a command is already running"); + return -EBUSY; + } + /* "help" answers with what this device runs: the console's own help + * would list every registered command, most of them refused here. + * Nothing is executed. */ + if (tedge_shell_is_help(cmd)) { + struct tedge_shell_event ev = { 0 }; + + (void)tedge_shell_help_text(TEDGE_SHELL_COMMAND_ALLOW_LIST, + ev.output, sizeof(ev.output)); + (void)tedge_port_msgq_put(&cmd_events, &ev); + return 0; + } + if (!tedge_shell_command_allowed(TEDGE_SHELL_COMMAND_ALLOW_LIST, cmd, + &why)) { + /* The command itself is not logged at INF: a refused command + * is someone else's text, and it lands in the cloud anyway. */ + TEDGE_LOG_WRN("shell: refused a command: %s", why); + snprintf(reason, rlen, "%s; \"help\" lists what this device runs", + why); + return -EACCES; + } + if (console_ready() != 0) { + snprintf(reason, rlen, "the console did not start"); + return -EIO; + } + /* The last command has finished (job.running is false); its thread + * may still be a moment from returning. */ + (void)tedge_port_thread_join(&cmd_thread, 1000); + + snprintf(job.cmd, sizeof(job.cmd), "%s", cmd); + job.started = tedge_port_uptime_ms(); + job.running = true; + job.reported = false; + TEDGE_LOG_INF("shell: running \"%s\"", job.cmd); + if (tedge_port_thread_start(&cmd_thread, cmd_stack, + TEDGE_PORT_STACK_SIZEOF(cmd_stack), + run_thread, NULL, "tedge_cmd") != 0) { + job.running = false; + snprintf(reason, rlen, "the command thread did not start"); + return -EIO; + } + return 0; +} + +int tedge_shell_poll_event(struct tedge_shell_event *ev) +{ + if (tedge_port_msgq_get(&cmd_events, ev) == 0) { + return 0; + } + /* A console command cannot be interrupted, so a timeout reports the + * operation as failed and leaves the thread to finish. The next + * command is refused until it does. */ + if (job.running && !job.reported && + tedge_port_uptime_ms() - job.started > + TEDGE_SHELL_COMMAND_TIMEOUT_S * 1000LL) { + job.reported = true; + memset(ev, 0, sizeof(*ev)); + ev->rc = -ETIMEDOUT; + snprintf(ev->output, sizeof(ev->output), + "the command did not finish within %d s and is still " + "running", + TEDGE_SHELL_COMMAND_TIMEOUT_S); + return 0; + } + return -ENOMSG; +} diff --git a/ports/freertos/include/tedge_port/os.h b/ports/freertos/include/tedge_port/os.h new file mode 100644 index 0000000..07670fb --- /dev/null +++ b/ports/freertos/include/tedge_port/os.h @@ -0,0 +1,131 @@ +/* SPDX-License-Identifier: Apache-2.0 */ + +/* FreeRTOS implementation of the types in . + * + * Every kernel object is created with FreeRTOS's *Static() calls into + * memory the object itself holds, so the client takes nothing from the + * FreeRTOS heap. The interface wants objects that are ready at file scope, + * which FreeRTOS cannot initialise at compile time; each object is + * therefore created on its first use (tedge_port_os.c), under the kernel's + * critical section. + * + * Only the FreeRTOS kernel API is used here and in tedge_port_os.c, so any + * FreeRTOS platform shares them; ESP-IDF's SMP FreeRTOS included, whose + * critical sections take a spinlock argument (TEDGE_FREERTOS_ENTER below). + * + * Needs configSUPPORT_STATIC_ALLOCATION and configUSE_TIMERS (both on in + * ESP-IDF). Event groups give the client 24 flags, of the 32 the interface + * allows; the core uses five. */ + +#ifndef TEDGE_PORT_FREERTOS_OS_H_ +#define TEDGE_PORT_FREERTOS_OS_H_ + +/* ESP-IDF keeps the kernel's headers under freertos/; mainline FreeRTOS + * puts them on the include path directly. */ +#if defined(ESP_PLATFORM) +#include +#include +#include +#include +#include +#include +#else +#include +#include +#include +#include +#include +#include +#endif + +#include +#include +#include + +#if !configSUPPORT_STATIC_ALLOCATION +#error "tiny-edge needs configSUPPORT_STATIC_ALLOCATION" +#endif +#if !configUSE_TIMERS +#error "tiny-edge needs configUSE_TIMERS (delayed work)" +#endif + +typedef struct { + StaticSemaphore_t buf; + SemaphoreHandle_t handle; +} tedge_port_mutex_t; +#define TEDGE_PORT_MUTEX_DEFINE(name) static tedge_port_mutex_t name + +/* One lock for every client spinlock: ESP-IDF's portMUX_TYPE is not valid + * as zeroed memory, which the interface allows. It is only held for a few + * copies, so sharing it costs nothing measurable. */ +typedef struct { + char unused; +} tedge_port_spinlock_t; +typedef UBaseType_t tedge_port_spinlock_key_t; + +typedef struct { + StaticTask_t tcb; + TaskHandle_t handle; + /* Given when entry returns; the task then suspends itself and the + * port deletes it (tedge_port_os.c explains why). */ + StaticSemaphore_t done_buf; + SemaphoreHandle_t done; + bool finished; + void (*entry)(void *arg); + void *arg; +} tedge_port_thread_t; + +/* ESP-IDF counts stack depth in bytes (StackType_t is uint8_t there); + * mainline FreeRTOS in words. Sizing the array in StackType_t serves both. */ +typedef StackType_t tedge_port_stack_t; +#define TEDGE_PORT_STACK_WORDS_(size) \ + (((size) + sizeof(StackType_t) - 1) / sizeof(StackType_t)) +#define TEDGE_PORT_STACK_DEFINE(name, size) \ + static tedge_port_stack_t name[TEDGE_PORT_STACK_WORDS_(size)] +#define TEDGE_PORT_STACK_ARRAY_DEFINE(name, n, size) \ + static tedge_port_stack_t name[n][TEDGE_PORT_STACK_WORDS_(size)] +#define TEDGE_PORT_STACK_SIZEOF(stack) sizeof(stack) + +typedef struct { + StaticQueue_t buf; + QueueHandle_t handle; + uint8_t *storage; + UBaseType_t msg_size; + UBaseType_t len; +} tedge_port_msgq_t; +/* Not static, as on Zephyr, whose K_MSGQ_DEFINE cannot be. */ +#define TEDGE_PORT_MSGQ_DEFINE(name, size_, n_) \ + static uint8_t name##_storage[(size_) * (n_)]; \ + tedge_port_msgq_t name = { .storage = name##_storage, \ + .msg_size = (size_), \ + .len = (n_) } + +typedef struct { + StaticEventGroup_t buf; + EventGroupHandle_t handle; +} tedge_port_event_t; +#define TEDGE_PORT_EVENT_DEFINE(name) static tedge_port_event_t name + +typedef struct { + StaticSemaphore_t buf; + SemaphoreHandle_t handle; +} tedge_port_sem_t; + +/* A one-shot software timer: the function runs on FreeRTOS's timer task, + * whose stack (configTIMER_TASK_STACK_DEPTH) must have room for it. */ +typedef struct { + StaticTimer_t buf; + TimerHandle_t handle; + void (*fn)(void); + const char *name; +} tedge_port_delayed_work_t; +#define TEDGE_PORT_DELAYED_WORK_DEFINE(id_, fn_) \ + static tedge_port_delayed_work_t id_ = { .fn = (fn_), .name = #id_ } + +/* Read and written with the compiler's __atomic built-ins (GCC, Clang), + * which every FreeRTOS toolchain the client targets has; on a CPU without + * atomic instructions (ESP32-C3) the toolchain's library provides them. */ +typedef intptr_t tedge_port_atomic_t; +#define TEDGE_PORT_ATOMIC_INIT(value) (value) + +#endif /* TEDGE_PORT_FREERTOS_OS_H_ */ diff --git a/ports/freertos/src/tedge_port_os.c b/ports/freertos/src/tedge_port_os.c new file mode 100644 index 0000000..3830bff --- /dev/null +++ b/ports/freertos/src/tedge_port_os.c @@ -0,0 +1,401 @@ +/* SPDX-License-Identifier: Apache-2.0 + * + * The FreeRTOS side of : kernel objects created with the + * *Static() calls on first use, a 64-bit uptime from the tick count, and + * the client's heap as an allowance on pvPortMalloc(). + * + * Only the FreeRTOS kernel API: what the platform adds (random numbers) + * is in the platform's own sources. + */ + +#include +#include +#include + +#include +#include + +#if !INCLUDE_vTaskSuspend || !INCLUDE_eTaskGetState || !INCLUDE_vTaskDelete +#error "tiny-edge needs INCLUDE_vTaskSuspend, INCLUDE_eTaskGetState and INCLUDE_vTaskDelete" +#endif + +TEDGE_LOG_DECLARE(); + +/* --- Critical sections --------------------------------------------------- */ + +/* ESP-IDF runs FreeRTOS on two cores, and its critical sections take a + * spinlock; mainline FreeRTOS's take nothing. */ +#if defined(ESP_PLATFORM) +static portMUX_TYPE tedge_mux = portMUX_INITIALIZER_UNLOCKED; +#define ENTER() taskENTER_CRITICAL(&tedge_mux) +#define EXIT() taskEXIT_CRITICAL(&tedge_mux) +#else +#define ENTER() taskENTER_CRITICAL() +#define EXIT() taskEXIT_CRITICAL() +#endif + +/* Create @p obj's kernel object on its first use. Two threads may race to + * be first; the critical section lets one create it. The *Static() calls + * only fill memory, which is allowed there. */ +#define ONCE(obj, create) \ + do { \ + if ((obj)->handle == NULL) { \ + ENTER(); \ + if ((obj)->handle == NULL) { \ + (obj)->handle = (create); \ + } \ + EXIT(); \ + } \ + } while (0) + +/* Milliseconds as ticks, rounded up so a short wait is never no wait, + * and kept below portMAX_DELAY, which means "for ever". */ +static TickType_t ticks(uint32_t ms) +{ + uint64_t t = ((uint64_t)ms * configTICK_RATE_HZ + 999) / 1000; + + return (t >= (uint64_t)portMAX_DELAY) ? portMAX_DELAY - 1 : (TickType_t)t; +} + +/* --- Mutexes ------------------------------------------------------------- */ + +void tedge_port_mutex_lock(tedge_port_mutex_t *mutex) +{ + ONCE(mutex, xSemaphoreCreateMutexStatic(&mutex->buf)); + (void)xSemaphoreTake(mutex->handle, portMAX_DELAY); +} + +void tedge_port_mutex_unlock(tedge_port_mutex_t *mutex) +{ + (void)xSemaphoreGive(mutex->handle); +} + +/* --- Spinlocks ---------------------------------------------------------- */ + +tedge_port_spinlock_key_t tedge_port_spin_lock(tedge_port_spinlock_t *lock) +{ + (void)lock; +#if defined(ESP_PLATFORM) + /* The _SAFE form works in a task and in an interrupt. */ + portENTER_CRITICAL_SAFE(&tedge_mux); + return 0; +#else + /* Raises the interrupt mask, in a task as in an interrupt. */ + return taskENTER_CRITICAL_FROM_ISR(); +#endif +} + +void tedge_port_spin_unlock(tedge_port_spinlock_t *lock, + tedge_port_spinlock_key_t key) +{ + (void)lock; +#if defined(ESP_PLATFORM) + (void)key; + portEXIT_CRITICAL_SAFE(&tedge_mux); +#else + taskEXIT_CRITICAL_FROM_ISR(key); +#endif +} + +/* --- Threads ------------------------------------------------------------- */ + +/* A FreeRTOS task cannot simply return, and one that deletes itself is + * only cleaned up later, by the idle task: its TCB and stack are still in + * the kernel's lists until then, and a new task on them would corrupt + * those lists. So a finished task suspends itself instead, and whoever + * joins it or reuses its memory deletes it: a task that is not running is + * deleted at once, also on ESP-IDF's two cores. */ +static void thread_trampoline(void *p) +{ + tedge_port_thread_t *thread = p; + + thread->entry(thread->arg); +#if INCLUDE_uxTaskGetStackHighWaterMark + /* What the thread left untouched of its stack, to size it. */ + TEDGE_LOG_DBG("thread %s ended: %u bytes of its stack never used", + pcTaskGetName(NULL), + (unsigned int)(uxTaskGetStackHighWaterMark(NULL) * + sizeof(StackType_t))); +#endif + __atomic_store_n(&thread->finished, true, __ATOMIC_RELEASE); + (void)xSemaphoreGive(thread->done); + vTaskSuspend(NULL); +} + +/* Delete @p thread's finished task, once it has suspended itself. + * @return 0, or -EBUSY when it is still running. */ +static int thread_reap(tedge_port_thread_t *thread) +{ + if (thread->handle == NULL) { + return 0; + } + if (!__atomic_load_n(&thread->finished, __ATOMIC_ACQUIRE)) { + return -EBUSY; + } + /* It is between giving "done" and suspending: a tick at most. */ + while (eTaskGetState(thread->handle) != eSuspended) { + vTaskDelay(1); + } + vTaskDelete(thread->handle); + thread->handle = NULL; + return 0; +} + +int tedge_port_thread_start(tedge_port_thread_t *thread, + tedge_port_stack_t *stack, size_t stack_size, + void (*entry)(void *arg), void *arg, + const char *name) +{ + int ret = thread_reap(thread); + + if (ret != 0) { + return ret; + } + if (thread->done == NULL) { + thread->done = xSemaphoreCreateBinaryStatic(&thread->done_buf); + } + (void)xSemaphoreTake(thread->done, 0); + thread->finished = false; + thread->entry = entry; + thread->arg = arg; + thread->handle = xTaskCreateStatic(thread_trampoline, name, + stack_size / sizeof(StackType_t), + thread, TEDGE_THREAD_PRIORITY, stack, + &thread->tcb); + return (thread->handle != NULL) ? 0 : -EINVAL; +} + +int tedge_port_thread_join(tedge_port_thread_t *thread, uint32_t timeout_ms) +{ + if (thread->handle == NULL) { + return 0; + } + if (!__atomic_load_n(&thread->finished, __ATOMIC_ACQUIRE) && + xSemaphoreTake(thread->done, ticks(timeout_ms)) != pdTRUE) { + return -EAGAIN; + } + return thread_reap(thread); +} + +void tedge_port_sleep_ms(uint32_t ms) +{ + vTaskDelay(ticks(ms)); +} + +/* --- Message queues ------------------------------------------------------ */ + +int tedge_port_msgq_put(tedge_port_msgq_t *queue, const void *msg) +{ + ONCE(queue, xQueueCreateStatic(queue->len, queue->msg_size, + queue->storage, &queue->buf)); + return (xQueueSend(queue->handle, msg, 0) == pdTRUE) ? 0 : -ENOMSG; +} + +int tedge_port_msgq_get(tedge_port_msgq_t *queue, void *msg) +{ + ONCE(queue, xQueueCreateStatic(queue->len, queue->msg_size, + queue->storage, &queue->buf)); + return (xQueueReceive(queue->handle, msg, 0) == pdTRUE) ? 0 : -ENOMSG; +} + +/* --- Event flags --------------------------------------------------------- */ + +/* An event group holds 24 flags with 32-bit ticks (the top byte is the + * kernel's own). */ +#define EVENT_BITS 0x00ffffffu + +static EventGroupHandle_t event_group(tedge_port_event_t *event) +{ + ONCE(event, xEventGroupCreateStatic(&event->buf)); + return event->handle; +} + +void tedge_port_event_post(tedge_port_event_t *event, uint32_t bits) +{ + configASSERT((bits & ~EVENT_BITS) == 0); + (void)xEventGroupSetBits(event_group(event), bits & EVENT_BITS); +} + +void tedge_port_event_clear(tedge_port_event_t *event, uint32_t bits) +{ + (void)xEventGroupClearBits(event_group(event), bits & EVENT_BITS); +} + +uint32_t tedge_port_event_test(tedge_port_event_t *event, uint32_t bits) +{ + return (uint32_t)xEventGroupGetBits(event_group(event)) & bits; +} + +uint32_t tedge_port_event_wait(tedge_port_event_t *event, uint32_t bits, + uint32_t timeout_ms) +{ + /* Any of the bits, left set: the return value holds the bits at the + * end of the wait, so a timeout leaves those asked for clear. */ + return (uint32_t)xEventGroupWaitBits(event_group(event), + bits & EVENT_BITS, pdFALSE, + pdFALSE, ticks(timeout_ms)) & + bits; +} + +/* --- Semaphores ---------------------------------------------------------- */ + +void tedge_port_sem_init(tedge_port_sem_t *sem, unsigned int count, + unsigned int limit) +{ + if (sem->handle != NULL) { + vSemaphoreDelete(sem->handle); /* static: only unregisters it */ + } + sem->handle = xSemaphoreCreateCountingStatic(limit, count, &sem->buf); +} + +void tedge_port_sem_give(tedge_port_sem_t *sem) +{ + /* At the limit the give fails, which is the interface's "up to". */ + (void)xSemaphoreGive(sem->handle); +} + +int tedge_port_sem_take(tedge_port_sem_t *sem, uint32_t timeout_ms) +{ + return (xSemaphoreTake(sem->handle, ticks(timeout_ms)) == pdTRUE) + ? 0 : -EAGAIN; +} + +void tedge_port_sem_reset(tedge_port_sem_t *sem) +{ + while (xSemaphoreTake(sem->handle, 0) == pdTRUE) { + } +} + +/* --- Delayed work -------------------------------------------------------- */ + +static void work_timer_fn(TimerHandle_t timer) +{ + tedge_port_delayed_work_t *work = pvTimerGetTimerID(timer); + + work->fn(); +} + +void tedge_port_delayed_work_schedule(tedge_port_delayed_work_t *work, + uint32_t delay_ms) +{ + TickType_t t = ticks(delay_ms); + + ONCE(work, xTimerCreateStatic(work->name, 1, pdFALSE, work, + work_timer_fn, &work->buf)); + /* Changing the period starts a stopped timer and restarts a running + * one: "reschedule it if pending". */ + (void)xTimerChangePeriod(work->handle, (t > 0) ? t : 1, portMAX_DELAY); +} + +void tedge_port_delayed_work_cancel(tedge_port_delayed_work_t *work) +{ + if (work->handle != NULL) { + (void)xTimerStop(work->handle, portMAX_DELAY); + } +} + +/* --- Atomics ------------------------------------------------------------- */ + +intptr_t tedge_port_atomic_get(const tedge_port_atomic_t *atomic) +{ + return __atomic_load_n(atomic, __ATOMIC_SEQ_CST); +} + +intptr_t tedge_port_atomic_set(tedge_port_atomic_t *atomic, intptr_t value) +{ + return __atomic_exchange_n(atomic, value, __ATOMIC_SEQ_CST); +} + +bool tedge_port_atomic_cas(tedge_port_atomic_t *atomic, intptr_t expected, + intptr_t desired) +{ + return __atomic_compare_exchange_n(atomic, &expected, desired, false, + __ATOMIC_SEQ_CST, __ATOMIC_SEQ_CST); +} + +/* --- Time ---------------------------------------------------------------- */ + +/* The tick count wraps (after 49 days at 1 kHz with 32-bit ticks); the + * wraps are counted here. The client asks far more often than that. */ +int64_t tedge_port_uptime_ms(void) +{ + static TickType_t last; + static uint64_t wraps; + uint64_t t; + + ENTER(); + TickType_t now = xTaskGetTickCount(); + + if (sizeof(TickType_t) < sizeof(uint64_t)) { + if (now < last) { + wraps++; + } + last = now; + t = (wraps << (8 * sizeof(TickType_t))) | now; + } else { + t = now; + } + EXIT(); + return (int64_t)(t * 1000 / configTICK_RATE_HZ); +} + +/* --- The client's heap --------------------------------------------------- */ + +/* An allowance of TEDGE_HEAP_SIZE bytes on the FreeRTOS heap: the client + * never takes more, and what it uses is counted. A header in front of each + * block records its size, aligned for any type. */ +typedef union { + size_t size; + long double align_ld; + void *align_p; + long long align_ll; +} heap_header; + +static size_t heap_used; + +void *tedge_port_heap_alloc(size_t size) +{ + heap_header *h; + + ENTER(); + if (size > TEDGE_HEAP_SIZE - heap_used) { + EXIT(); + return NULL; + } + heap_used += size; + EXIT(); + + h = pvPortMalloc(sizeof(*h) + size); + if (h == NULL) { + ENTER(); + heap_used -= size; + EXIT(); + return NULL; + } + h->size = size; + return h + 1; +} + +void tedge_port_heap_free(void *p) +{ + heap_header *h; + + if (p == NULL) { + return; + } + h = (heap_header *)p - 1; + ENTER(); + heap_used -= h->size; + EXIT(); + vPortFree(h); +} + +size_t tedge_port_heap_free_bytes(void) +{ + size_t free_bytes; + + ENTER(); + free_bytes = TEDGE_HEAP_SIZE - heap_used; + EXIT(); + return free_bytes; +} diff --git a/ports/posix/CMakeLists.txt b/ports/posix/CMakeLists.txt index a6a253d..b93be36 100644 --- a/ports/posix/CMakeLists.txt +++ b/ports/posix/CMakeLists.txt @@ -49,6 +49,9 @@ add_library(tedge STATIC src/tedge_port_kv.c src/tedge_port_net.c src/tedge_port_crypto.c + # Shared with other ports that have BSD sockets and mbedTLS. + ${TEDGE_ROOT}/ports/common/src/tedge_sockets.c + ${TEDGE_ROOT}/ports/common/src/tedge_mbedtls_key.c ) add_library(tedge::tedge ALIAS tedge) @@ -74,30 +77,17 @@ target_include_directories(tedge PUBLIC ) target_include_directories(tedge PRIVATE ${TEDGE_ROOT}/core/src + ${TEDGE_ROOT}/ports/common/include ${CMAKE_CURRENT_BINARY_DIR}/generated ) target_link_libraries(tedge PUBLIC ${TEDGE_MBEDTLS_LIBRARIES} Threads::Threads) tedge_generate_config(${CMAKE_CURRENT_BINARY_DIR}/generated) -# Trust anchors: every PEM in TEDGE_C8Y_CA_FILES, concatenated and embedded -# as tedge_ca_bundle.inc (comma-separated bytes), as the Zephyr port does. -separate_arguments(_ca_files UNIX_COMMAND "${TEDGE_C8Y_CA_FILES}") -set(_bundle "") -foreach(_pem ${_ca_files}) - if(NOT IS_ABSOLUTE ${_pem}) - set(_pem ${TEDGE_ROOT}/${_pem}) - endif() - if(NOT EXISTS ${_pem}) - message(FATAL_ERROR "TEDGE_C8Y_CA_FILES: no such file: ${_pem}") - endif() - file(READ ${_pem} _content HEX) - string(APPEND _bundle "${_content}") - set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS ${_pem}) -endforeach() -string(REGEX REPLACE "([0-9a-f][0-9a-f])" "0x\\1," _bundle "${_bundle}") -file(CONFIGURE OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/generated/tedge_ca_bundle.inc - CONTENT "${_bundle}\n") +# Trust anchors: every PEM in TEDGE_C8Y_CA_FILES, embedded as +# tedge_ca_bundle.inc, as the other ports do. +include(${TEDGE_ROOT}/ports/common/cmake/tedge_ca_bundle.cmake) +tedge_ca_bundle(${CMAKE_CURRENT_BINARY_DIR}/generated "${TEDGE_C8Y_CA_FILES}") if(TEDGE_POSIX_SAMPLES) add_subdirectory(samples/minimal) diff --git a/ports/posix/src/tedge_port_crypto.c b/ports/posix/src/tedge_port_crypto.c index d21b399..6f77428 100644 --- a/ports/posix/src/tedge_port_crypto.c +++ b/ports/posix/src/tedge_port_crypto.c @@ -1,174 +1,61 @@ /* SPDX-License-Identifier: Apache-2.0 * - * The POSIX side of : the device key as a PEM file in - * the state directory, readable by the owner only, as thin-edge.io keeps - * its keys on Linux (device-key.pem); a CSR signed with it by mbedTLS; and - * the system's random numbers. + * The POSIX side of : where the device key is kept, a + * PEM file in the state directory readable by the owner only, as + * thin-edge.io keeps its keys on Linux (device-key.pem); and the system's + * random numbers. Generating the key and signing the CSR are shared with + * other ports (ports/common/src/tedge_mbedtls_key.c). */ #include -#include +#include "tedge_common.h" #include "tedge_posix_internal.h" -#include "tedge_posix_tls.h" - -#include -#include -#include -#include #include #include -#include -#include #include #include /* getentropy() on glibc */ -TEDGE_LOG_DECLARE(); +/* PSA's key slots and random generator are shared between the client's + * threads, which are pthreads. */ +#if !defined(MBEDTLS_THREADING_PTHREAD) +#error "tiny-edge needs mbedTLS built with MBEDTLS_THREADING_PTHREAD" +#endif #define KEY_FILE "device-key.pem" -/* A P-256 key in PEM is about 230 bytes. */ -#define KEY_PEM_MAX 512 -static pthread_once_t psa_once = PTHREAD_ONCE_INIT; -static psa_status_t psa_init_status; +/* The path is made on each call: the state directory is the + * application's to set (tedge_posix_configure()), and tests change it. */ +static char key_file[PATH_MAX]; -static void psa_init(void) +const char *tedge_common_key_where(void) { - psa_init_status = psa_crypto_init(); -} - -int tedge_posix_psa_init(void) -{ - (void)pthread_once(&psa_once, psa_init); - if (psa_init_status != PSA_SUCCESS) { - TEDGE_LOG_ERR("PSA crypto did not start (%d)", (int)psa_init_status); - return -EIO; + if (tedge_posix_path(key_file, sizeof(key_file), KEY_FILE) != 0) { + return KEY_FILE; } - return 0; + return key_file; } -static int key_path(char *buf, size_t len) +int tedge_common_key_load(char *pem, size_t cap) { - return tedge_posix_path(buf, len, KEY_FILE); -} - -int tedge_posix_device_pk(mbedtls_pk_context *pk) -{ - unsigned char pem[KEY_PEM_MAX]; char path[PATH_MAX]; ssize_t n; - int ret = tedge_posix_psa_init(); + int ret = tedge_posix_path(path, sizeof(path), KEY_FILE); - if (ret == 0) { - ret = key_path(path, sizeof(path)); - } if (ret != 0) { return ret; } - n = tedge_posix_read_file(path, pem, sizeof(pem) - 1); - if (n < 0) { - return (int)n; - } - if ((size_t)n >= sizeof(pem)) { - return -EFBIG; - } - pem[n] = '\0'; - /* PEM is parsed with its NUL. */ - ret = mbedtls_pk_parse_key(pk, pem, (size_t)n + 1, NULL, 0); - mbedtls_platform_zeroize(pem, sizeof(pem)); - if (ret != 0) { - TEDGE_LOG_ERR("the device key (%s) is not usable (-0x%04x)", path, - (unsigned int)-ret); - return -EINVAL; - } - return 0; + n = tedge_posix_read_file(path, pem, cap); + return (n > INT_MAX) ? -EFBIG : (int)n; } -int tedge_port_device_key_ensure(void) +int tedge_common_key_store(const char *pem, size_t len) { - psa_key_attributes_t attr = PSA_KEY_ATTRIBUTES_INIT; - unsigned char pem[KEY_PEM_MAX]; - mbedtls_pk_context pk; char path[PATH_MAX]; - psa_key_id_t id = 0; - psa_status_t st; - int ret; - - mbedtls_pk_init(&pk); - ret = tedge_posix_device_pk(&pk); - mbedtls_pk_free(&pk); - if (ret != -ENOENT) { - return ret; /* 0: generated on an earlier start */ - } - - /* Generated in PSA, written out once as PEM, then forgotten by PSA: - * the file is the key's only home. */ - psa_set_key_type(&attr, PSA_KEY_TYPE_ECC_KEY_PAIR(PSA_ECC_FAMILY_SECP_R1)); - psa_set_key_bits(&attr, 256); - psa_set_key_usage_flags(&attr, PSA_KEY_USAGE_SIGN_HASH | - PSA_KEY_USAGE_SIGN_MESSAGE | - PSA_KEY_USAGE_EXPORT); - psa_set_key_algorithm(&attr, PSA_ALG_ECDSA(PSA_ALG_SHA_256)); - st = psa_generate_key(&attr, &id); - psa_reset_key_attributes(&attr); - if (st != PSA_SUCCESS) { - TEDGE_LOG_ERR("generating the device key failed (%d)", (int)st); - return -EIO; - } - mbedtls_pk_init(&pk); - ret = mbedtls_pk_copy_from_psa(id, &pk); - if (ret == 0) { - ret = mbedtls_pk_write_key_pem(&pk, pem, sizeof(pem)); - } - mbedtls_pk_free(&pk); - (void)psa_destroy_key(id); - if (ret != 0) { - TEDGE_LOG_ERR("exporting the device key failed (-0x%04x)", - (unsigned int)-ret); - return -EIO; - } - ret = key_path(path, sizeof(path)); - if (ret == 0) { - ret = tedge_posix_write_file(path, pem, strlen((char *)pem), 0600); - } - mbedtls_platform_zeroize(pem, sizeof(pem)); - if (ret != 0) { - TEDGE_LOG_ERR("could not store the device key in %s (%d)", path, ret); - return ret; - } - TEDGE_LOG_INF("device key generated (P-256, %s)", path); - return 0; -} - -int tedge_port_device_csr(const char *subject, char *pem, size_t cap) -{ - mbedtls_x509write_csr csr; - mbedtls_pk_context pk; - int ret; + int ret = tedge_posix_path(path, sizeof(path), KEY_FILE); - mbedtls_x509write_csr_init(&csr); - mbedtls_pk_init(&pk); - ret = tedge_posix_device_pk(&pk); - if (ret != 0) { - goto out; - } - mbedtls_x509write_csr_set_md_alg(&csr, MBEDTLS_MD_SHA256); - mbedtls_x509write_csr_set_key(&csr, &pk); - ret = mbedtls_x509write_csr_set_subject_name(&csr, subject); - if (ret == 0) { - ret = mbedtls_x509write_csr_pem(&csr, (unsigned char *)pem, cap); - } - if (ret != 0) { - TEDGE_LOG_ERR("writing the CSR failed (-0x%04x)", (unsigned int)-ret); - ret = -EIO; - goto out; - } - ret = (int)strlen(pem); -out: - mbedtls_pk_free(&pk); - mbedtls_x509write_csr_free(&csr); - return ret; + return (ret == 0) ? tedge_posix_write_file(path, pem, len, 0600) : ret; } int tedge_port_random(void *buf, size_t len) diff --git a/ports/posix/src/tedge_port_net.c b/ports/posix/src/tedge_port_net.c index c89fbbc..c88bf33 100644 --- a/ports/posix/src/tedge_port_net.c +++ b/ports/posix/src/tedge_port_net.c @@ -1,144 +1,29 @@ /* SPDX-License-Identifier: Apache-2.0 * - * The POSIX side of : BSD sockets, TLS with mbedTLS, - * the system's resolver, and the interface list (getifaddrs()). - * - * A connection handle is a slot in a table of the port's own, not the - * socket itself: a TLS connection carries its mbedTLS session beside the - * socket. Each slot is used by one thread at a time (the interface's - * rule), so only taking and freeing slots needs the table's lock. - * - * Every socket is non-blocking, and every wait is a poll() with the time - * left, so a timeout holds whether TLS or the kernel is waiting. + * The POSIX side of : the interface queries, from the + * interface list (getifaddrs()). The connections and TLS are shared with + * other ports (ports/common/src/tedge_sockets.c). */ -#include #include #include +#include "tedge_common.h" #include "tedge_posix_internal.h" -#include "tedge_posix_tls.h" - -#include -#include -#include -#include -#include #include #include -#include #include -#include -#include #include #include #include -#include -#include - -TEDGE_LOG_DECLARE(); - -/* More than the client holds at once: MQTT, a download or an upload, - * enrollment, and the remote-access sessions with their targets. */ -#define MAX_CONNS 16 -/* How long a send may wait for room in the socket before the connection is - * given up as dead. */ -#define SEND_TIMEOUT_MS 30000 - -#if defined(MSG_NOSIGNAL) -#define SEND_FLAGS MSG_NOSIGNAL -#else -#define SEND_FLAGS 0 /* SO_NOSIGPIPE on the socket instead (macOS) */ -#endif - -struct conn { - bool used; - bool tls; - int fd; - mbedtls_ssl_context ssl; - mbedtls_ssl_config conf; - mbedtls_x509_crt ca; - mbedtls_x509_crt own; - mbedtls_pk_context key; -}; - -static struct conn conns[MAX_CONNS]; -static pthread_mutex_t conns_lock = PTHREAD_MUTEX_INITIALIZER; - -static int64_t now_ms(void) -{ - struct timespec ts; - - (void)clock_gettime(CLOCK_MONOTONIC, &ts); - return (int64_t)ts.tv_sec * 1000 + ts.tv_nsec / 1000000; -} - -static struct conn *conn_get(int handle) -{ - if (handle < 0 || handle >= MAX_CONNS || !conns[handle].used) { - return NULL; - } - return &conns[handle]; -} - -static int conn_alloc(int fd, bool tls) -{ - int handle = -ENFILE; - - (void)pthread_mutex_lock(&conns_lock); - for (int i = 0; i < MAX_CONNS; i++) { - if (!conns[i].used) { - conns[i].used = true; - conns[i].tls = tls; - conns[i].fd = fd; - if (tls) { - /* Before anything can fail, so conn_free() - * always frees initialised state. */ - mbedtls_ssl_init(&conns[i].ssl); - mbedtls_ssl_config_init(&conns[i].conf); - mbedtls_x509_crt_init(&conns[i].ca); - mbedtls_x509_crt_init(&conns[i].own); - mbedtls_pk_init(&conns[i].key); - } - handle = i; - break; - } - } - (void)pthread_mutex_unlock(&conns_lock); - return handle; -} - -static void conn_free(struct conn *c) -{ - if (c->tls) { - mbedtls_ssl_free(&c->ssl); - mbedtls_ssl_config_free(&c->conf); - mbedtls_x509_crt_free(&c->ca); - mbedtls_x509_crt_free(&c->own); - mbedtls_pk_free(&c->key); - } - if (c->fd >= 0) { - (void)close(c->fd); - } - (void)pthread_mutex_lock(&conns_lock); - c->used = false; - c->tls = false; - c->fd = -1; - (void)pthread_mutex_unlock(&conns_lock); -} -/* A socket as every connection here has it: non-blocking, closed on exec, - * and never raising SIGPIPE. */ -static int socket_open(int type) +/* Every socket the client opens is closed in the child of an exec (a + * restart), and never raises SIGPIPE: MSG_NOSIGNAL where send() has it, + * SO_NOSIGPIPE on the socket where it does not (macOS). */ +void tedge_common_socket_opened(int fd) { - int fd = socket(AF_INET, type, 0); - - if (fd < 0) { - return -errno; - } tedge_posix_cloexec(fd); - (void)fcntl(fd, F_SETFL, fcntl(fd, F_GETFL) | O_NONBLOCK); #if defined(SO_NOSIGPIPE) { int on = 1; @@ -146,32 +31,6 @@ static int socket_open(int type) (void)setsockopt(fd, SOL_SOCKET, SO_NOSIGPIPE, &on, sizeof(on)); } #endif - return fd; -} - -/* Wait until @p fd is ready for @p events or @p deadline passes. - * @return 0 when ready, -EAGAIN at the deadline, or a negative errno. */ -static int wait_fd(int fd, short events, int64_t deadline) -{ - for (;;) { - struct pollfd pfd = { .fd = fd, .events = events }; - int64_t left = deadline - now_ms(); - int ret; - - if (left <= 0) { - return -EAGAIN; - } - ret = poll(&pfd, 1, (int)left); - if (ret > 0) { - return 0; - } - if (ret == 0) { - return -EAGAIN; - } - if (errno != EINTR) { - return -errno; - } - } } /* --- Connectivity -------------------------------------------------------- */ @@ -258,51 +117,6 @@ void tedge_port_net_watch(void (*cb)(bool up)) } } -/* --- Names --------------------------------------------------------------- */ - -bool tedge_port_ipv4_parse(const char *text, uint8_t addr[4]) -{ - struct in_addr in; - - if (inet_pton(AF_INET, text, &in) != 1) { - return false; - } - memcpy(addr, &in, 4); - return true; -} - -/* getaddrinfo(), which follows the system's configuration (resolv.conf, - * nsswitch.conf, mDNS on macOS). It cannot be cancelled, so the timeout is - * the resolver's own (resolv.conf's "timeout" and "attempts"), not - * @p timeout_ms. */ -int tedge_port_resolve(const char *host, uint8_t addr[4], uint32_t timeout_ms) -{ - struct addrinfo hints = { .ai_family = AF_INET, - .ai_socktype = SOCK_STREAM }; - struct addrinfo *res; - int ret; - - (void)timeout_ms; - if (tedge_port_ipv4_parse(host, addr)) { - return 0; - } - ret = getaddrinfo(host, NULL, &hints, &res); - switch (ret) { - case 0: - memcpy(addr, &((struct sockaddr_in *)(void *)res->ai_addr)->sin_addr, - 4); - freeaddrinfo(res); - return 0; - case EAI_AGAIN: /* the servers did not answer */ - return -ETIMEDOUT; - case EAI_MEMORY: - case EAI_SYSTEM: - return -EAGAIN; - default: /* EAI_NONAME, EAI_FAIL, ...: the next try says the same */ - return -ENOENT; - } -} - static bool same_addr_fn(const struct sockaddr_in *addr, const struct sockaddr_in *mask, unsigned int flags, const void *ctx) @@ -337,418 +151,3 @@ bool tedge_port_net_on_subnet(const uint8_t addr[4]) { return each_ipv4(true, subnet_fn, addr); } - -/* --- TLS credentials ----------------------------------------------------- */ - -/* The buffers stay the caller's (the interface's rule). Each TLS - * connection parses what is set when it opens, so a change of credentials - * never pulls them from under a session. */ -static pthread_mutex_t cred_lock = PTHREAD_MUTEX_INITIALIZER; -static const void *ca_pem; -static size_t ca_len; -static const void *device_cert; -static size_t device_cert_len; - -int tedge_port_tls_set_ca(const void *pem, size_t len) -{ - (void)pthread_mutex_lock(&cred_lock); - ca_pem = pem; - ca_len = len; - (void)pthread_mutex_unlock(&cred_lock); - return 0; -} - -int tedge_port_tls_set_device(const void *cert, size_t cert_len) -{ - (void)pthread_mutex_lock(&cred_lock); - device_cert = cert; - device_cert_len = cert_len; - (void)pthread_mutex_unlock(&cred_lock); - return 0; -} - -void tedge_port_tls_clear_device(void) -{ - (void)pthread_mutex_lock(&cred_lock); - device_cert = NULL; - device_cert_len = 0; - (void)pthread_mutex_unlock(&cred_lock); -} - -/* --- TLS over a socket --------------------------------------------------- */ - -static int bio_send(void *ctx, const unsigned char *buf, size_t len) -{ - struct conn *c = ctx; - ssize_t n = send(c->fd, buf, len, SEND_FLAGS); - - if (n >= 0) { - return (int)n; - } - if (errno == EAGAIN || errno == EWOULDBLOCK || errno == EINTR) { - return MBEDTLS_ERR_SSL_WANT_WRITE; - } - return (errno == EPIPE || errno == ECONNRESET) ? MBEDTLS_ERR_NET_CONN_RESET - : MBEDTLS_ERR_NET_SEND_FAILED; -} - -static int bio_recv(void *ctx, unsigned char *buf, size_t len) -{ - struct conn *c = ctx; - ssize_t n = recv(c->fd, buf, len, 0); - - if (n >= 0) { - return (int)n; - } - if (errno == EAGAIN || errno == EWOULDBLOCK || errno == EINTR) { - return MBEDTLS_ERR_SSL_WANT_READ; - } - return (errno == ECONNRESET) ? MBEDTLS_ERR_NET_CONN_RESET - : MBEDTLS_ERR_NET_RECV_FAILED; -} - -static int tls_setup(struct conn *c, enum tedge_port_security security, - const char *hostname) -{ - const char *what = "CA bundle"; - int ret; - - ret = tedge_posix_psa_init(); - if (ret != 0) { - return ret; - } - (void)pthread_mutex_lock(&cred_lock); - ret = (ca_pem != NULL) - ? mbedtls_x509_crt_parse(&c->ca, ca_pem, ca_len) - : MBEDTLS_ERR_X509_BAD_INPUT_DATA; - if (ret == 0 && security == TEDGE_PORT_TLS_DEVICE) { - what = "device certificate"; - ret = (device_cert != NULL) - ? mbedtls_x509_crt_parse_der(&c->own, device_cert, - device_cert_len) - : MBEDTLS_ERR_X509_BAD_INPUT_DATA; - } - (void)pthread_mutex_unlock(&cred_lock); - if (ret != 0) { - TEDGE_LOG_ERR("TLS: the %s is missing or not valid (-0x%04x)", - what, (unsigned int)-ret); - return -EINVAL; - } - if (security == TEDGE_PORT_TLS_DEVICE) { - ret = tedge_posix_device_pk(&c->key); - if (ret != 0) { - return ret; - } - } - - ret = mbedtls_ssl_config_defaults(&c->conf, MBEDTLS_SSL_IS_CLIENT, - MBEDTLS_SSL_TRANSPORT_STREAM, - MBEDTLS_SSL_PRESET_DEFAULT); - if (ret != 0) { - return -ENOMEM; - } - /* TLS 1.2, as the Zephyr port connects. */ - mbedtls_ssl_conf_max_tls_version(&c->conf, MBEDTLS_SSL_VERSION_TLS1_2); - mbedtls_ssl_conf_authmode(&c->conf, MBEDTLS_SSL_VERIFY_REQUIRED); - mbedtls_ssl_conf_ca_chain(&c->conf, &c->ca, NULL); - if (security == TEDGE_PORT_TLS_DEVICE && - mbedtls_ssl_conf_own_cert(&c->conf, &c->own, &c->key) != 0) { - return -ENOMEM; - } - if (mbedtls_ssl_setup(&c->ssl, &c->conf) != 0 || - mbedtls_ssl_set_hostname(&c->ssl, hostname) != 0) { - return -ENOMEM; - } - mbedtls_ssl_set_bio(&c->ssl, c, bio_send, bio_recv, NULL); - return 0; -} - -static int tls_handshake(struct conn *c, const char *hostname, int64_t deadline) -{ - for (;;) { - int ret = mbedtls_ssl_handshake(&c->ssl); - - if (ret == 0) { - return 0; - } - if (ret == MBEDTLS_ERR_SSL_WANT_READ || - ret == MBEDTLS_ERR_SSL_WANT_WRITE) { - ret = wait_fd(c->fd, - (ret == MBEDTLS_ERR_SSL_WANT_READ) ? POLLIN - : POLLOUT, - deadline); - if (ret == -EAGAIN) { - TEDGE_LOG_WRN("TLS: %s: the handshake timed out", - hostname); - return -ETIMEDOUT; - } - if (ret != 0) { - return ret; - } - continue; - } - if (ret == MBEDTLS_ERR_X509_CERT_VERIFY_FAILED) { - char info[160]; - - (void)mbedtls_x509_crt_verify_info( - info, sizeof(info), "", - mbedtls_ssl_get_verify_result(&c->ssl)); - info[strcspn(info, "\n")] = '\0'; - TEDGE_LOG_ERR("TLS: %s: the server's certificate is not " - "trusted: %s", hostname, info); - return -EACCES; - } - TEDGE_LOG_WRN("TLS: %s: the handshake failed (-0x%04x)", hostname, - (unsigned int)-ret); - return -ECONNABORTED; - } -} - -/* --- Connections --------------------------------------------------------- */ - -int tedge_port_connect(const uint8_t addr[4], uint16_t port, - enum tedge_port_security security, const char *hostname, - uint32_t timeout_ms) -{ - struct sockaddr_in sa = { .sin_family = AF_INET, - .sin_port = htons(port) }; - int64_t deadline = now_ms() + timeout_ms; - bool tls = security != TEDGE_PORT_TCP; - struct conn *c; - int fd, handle, ret; - - memcpy(&sa.sin_addr, addr, 4); - fd = socket_open(SOCK_STREAM); - if (fd < 0) { - return fd; - } - handle = conn_alloc(fd, tls); - if (handle < 0) { - (void)close(fd); - return handle; - } - c = &conns[handle]; - - if (connect(fd, (struct sockaddr *)&sa, sizeof(sa)) != 0) { - if (errno != EINPROGRESS) { - ret = -errno; - goto fail; - } - ret = wait_fd(fd, POLLOUT, deadline); - if (ret != 0) { - ret = (ret == -EAGAIN) ? -ETIMEDOUT : ret; - goto fail; - } - { - int err = 0; - socklen_t len = sizeof(err); - - (void)getsockopt(fd, SOL_SOCKET, SO_ERROR, &err, &len); - if (err != 0) { - ret = -err; - goto fail; - } - } - } - if (tls) { - ret = tls_setup(c, security, hostname); - if (ret == 0) { - ret = tls_handshake(c, hostname, deadline); - } - if (ret != 0) { - goto fail; - } - } - return handle; - -fail: - conn_free(c); - return ret; -} - -int tedge_port_udp_open(void) -{ - int fd = socket_open(SOCK_DGRAM); - int handle; - - if (fd < 0) { - return fd; - } - handle = conn_alloc(fd, false); - if (handle < 0) { - (void)close(fd); - } - return handle; -} - -int tedge_port_udp_sendto(int conn, const void *buf, size_t len, - const uint8_t addr[4], uint16_t port) -{ - struct sockaddr_in sa = { .sin_family = AF_INET, - .sin_port = htons(port) }; - struct conn *c = conn_get(conn); - - if (c == NULL) { - return -EBADF; - } - memcpy(&sa.sin_addr, addr, 4); - return (sendto(c->fd, buf, len, SEND_FLAGS, (struct sockaddr *)&sa, - sizeof(sa)) < 0) ? -errno : 0; -} - -int tedge_port_set_nodelay(int conn) -{ - struct conn *c = conn_get(conn); - int on = 1; - - if (c == NULL) { - return -EBADF; - } - return (setsockopt(c->fd, IPPROTO_TCP, TCP_NODELAY, &on, sizeof(on)) < 0) - ? -errno : 0; -} - -int tedge_port_send(int conn, const void *buf, size_t len) -{ - int64_t deadline = now_ms() + SEND_TIMEOUT_MS; - struct conn *c = conn_get(conn); - - if (c == NULL) { - return -EBADF; - } - for (;;) { - short want = POLLOUT; - int ret; - - if (c->tls) { - ret = mbedtls_ssl_write(&c->ssl, buf, len); - if (ret >= 0) { - return ret; - } - if (ret == MBEDTLS_ERR_SSL_WANT_READ) { - want = POLLIN; - } else if (ret != MBEDTLS_ERR_SSL_WANT_WRITE) { - return -ECONNRESET; - } - } else { - ssize_t n = send(c->fd, buf, len, SEND_FLAGS); - - if (n >= 0) { - return (int)n; - } - if (errno != EAGAIN && errno != EWOULDBLOCK && - errno != EINTR) { - return -errno; - } - } - ret = wait_fd(c->fd, want, deadline); - if (ret != 0) { - return (ret == -EAGAIN) ? -ETIMEDOUT : ret; - } - } -} - -int tedge_port_recv(int conn, void *buf, size_t len, uint32_t timeout_ms) -{ - int64_t deadline = now_ms() + timeout_ms; - struct conn *c = conn_get(conn); - - if (c == NULL) { - return -EBADF; - } - for (;;) { - short want = POLLIN; - int ret; - - if (c->tls) { - ret = mbedtls_ssl_read(&c->ssl, buf, len); - if (ret >= 0) { - return ret; /* 0: the peer closed */ - } - if (ret == MBEDTLS_ERR_SSL_PEER_CLOSE_NOTIFY) { - return 0; - } - if (ret == MBEDTLS_ERR_SSL_WANT_WRITE) { - want = POLLOUT; - } else if (ret != MBEDTLS_ERR_SSL_WANT_READ) { - return -ECONNRESET; - } - } else { - ssize_t n = recv(c->fd, buf, len, 0); - - if (n >= 0) { - return (int)n; - } - if (errno != EAGAIN && errno != EWOULDBLOCK && - errno != EINTR) { - return -errno; - } - } - if (timeout_ms == 0) { - return -EAGAIN; - } - ret = wait_fd(c->fd, want, deadline); - if (ret != 0) { - return ret; - } - } -} - -int tedge_port_poll(struct tedge_port_pollfd *fds, size_t n, - uint32_t timeout_ms) -{ - struct pollfd pfds[8]; - int buffered = 0; - int ret; - - if (n > sizeof(pfds) / sizeof(pfds[0])) { - return -EINVAL; - } - for (size_t i = 0; i < n; i++) { - struct conn *c = conn_get(fds[i].conn); - - fds[i].revents = 0; - pfds[i].fd = (c != NULL) ? c->fd : -1; - pfds[i].events = (fds[i].events & TEDGE_PORT_POLLIN) ? POLLIN : 0; - pfds[i].revents = 0; - /* TLS may hold a decrypted record the socket no longer shows. */ - if (c != NULL && c->tls && (fds[i].events & TEDGE_PORT_POLLIN) && - mbedtls_ssl_get_bytes_avail(&c->ssl) > 0) { - fds[i].revents = TEDGE_PORT_POLLIN; - buffered++; - } - } - do { - ret = poll(pfds, n, buffered > 0 ? 0 : (int)timeout_ms); - } while (ret < 0 && errno == EINTR); - if (ret < 0) { - return -errno; - } - ret = 0; - for (size_t i = 0; i < n; i++) { - fds[i].revents |= - ((pfds[i].revents & POLLIN) ? TEDGE_PORT_POLLIN : 0) | - ((pfds[i].revents & (POLLERR | POLLNVAL)) ? TEDGE_PORT_POLLERR - : 0) | - ((pfds[i].revents & POLLHUP) ? TEDGE_PORT_POLLHUP : 0); - if (fds[i].revents != 0) { - ret++; - } - } - return ret; -} - -void tedge_port_close(int conn) -{ - struct conn *c = conn_get(conn); - - if (c == NULL) { - return; - } - if (c->tls) { - /* Best effort: the socket is non-blocking, and a peer that is - * gone does not hold the close up. */ - (void)mbedtls_ssl_close_notify(&c->ssl); - } - conn_free(c); -} diff --git a/ports/posix/src/tedge_posix_tls.h b/ports/posix/src/tedge_posix_tls.h deleted file mode 100644 index 77c1c48..0000000 --- a/ports/posix/src/tedge_posix_tls.h +++ /dev/null @@ -1,24 +0,0 @@ -/* SPDX-License-Identifier: Apache-2.0 */ - -/* The POSIX port's mbedTLS glue, shared by its crypto and network sources; - * not part of the port interface. */ - -#ifndef TEDGE_POSIX_TLS_H_ -#define TEDGE_POSIX_TLS_H_ - -#include - -/* Several client threads hold TLS sessions at once, and PSA's key slots - * and random generator are shared between them. */ -#if !defined(MBEDTLS_THREADING_C) || !defined(MBEDTLS_THREADING_PTHREAD) -#error "tiny-edge needs mbedTLS built with MBEDTLS_THREADING_C and MBEDTLS_THREADING_PTHREAD" -#endif - -/** Start PSA crypto, once. @return 0, or -EIO. */ -int tedge_posix_psa_init(void); - -/** Load the device key into @p pk (initialised by the caller). - * @return 0; -ENOENT when there is no key yet; or another negative errno. */ -int tedge_posix_device_pk(mbedtls_pk_context *pk); - -#endif /* TEDGE_POSIX_TLS_H_ */ diff --git a/scripts/check-portability.py b/scripts/check-portability.py index 9bc015b..b7c9bdb 100755 --- a/scripts/check-portability.py +++ b/scripts/check-portability.py @@ -13,6 +13,9 @@ shares, so they may only use C, the client's own headers and the port interface. 2. A port never reaches into another port (ports/ must not name ports/). + ports/common/ is not a port: it is code for platforms with BSD sockets and + mbedTLS, which any port may build. It names no port, and is held to rule + 1 as well, since several RTOSes share it. 3. Nothing reaches outside the repository with "../" paths. Documentation (*.md) is not checked. @@ -57,8 +60,13 @@ def is_code(path: Path) -> bool: or path.suffix in CODE_SUFFIXES +# Shared by the ports, not one of them. +COMMON = "common" + + def is_portable(rel: str) -> bool: - return rel.startswith(("core/", "provisioning/", "include/tedge/port/")) \ + return rel.startswith(("core/", "provisioning/", "include/tedge/port/", + f"ports/{COMMON}/")) \ or rel in ("include/tedge/tedge.h", "include/tedge/provisioning.h") @@ -89,7 +97,8 @@ def check_file(path: Path) -> list[str]: own = rel.split("/")[1] for lineno, line in enumerate(text.splitlines(), start=1): for match in PORT_REF.finditer(line): - if match.group(1) != own: + if match.group(1) != own and (match.group(1) != COMMON + or own == COMMON): problems.append(f"{rel}:{lineno}: port '{own}' refers to " f"port '{match.group(1)}'")