diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..dd95613 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +build +main/wifi_cfg.hpp +CMakeFiles +CMakeCache.txt +sdkconfig +sdkconfig.old +main/broker.pem diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..12393e9 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,8 @@ +# The following lines of boilerplate have to be in your project's +# CMakeLists in this exact order for cmake to work correctly +cmake_minimum_required(VERSION 3.16) + +include($ENV{IDF_PATH}/tools/cmake/project.cmake) +project(blinky) + +target_add_binary_data(blinky.elf "main/broker.pem" TEXT) diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt new file mode 100644 index 0000000..84078a2 --- /dev/null +++ b/main/CMakeLists.txt @@ -0,0 +1,29 @@ +idf_component_register( + SRCS + "main.cpp" + "wifi.cpp" + "http_serv.cpp" + "ota.cpp" + "mqtt.cpp" + + INCLUDE_DIRS "." + ) + +# Build static library, do not build test executables +option(BUILD_SHARED_LIBS OFF) +option(BUILD_TESTING OFF) + +# Unfortunately the library performs install and export. Would +# have been nice if devs made that an option like BUILD_SHARED_LIBS +# and BUILD_TESTING. Override install() and export() to do nothing +# instead. +function(install) +endfunction() + +function(export) +endfunction() + +# Import tinyxml2 targets +#add_subdirectory(lib/tinyxml2) +# Link tinyxml2 to main component +#target_link_libraries(${COMPONENT_LIB} PUBLIC tinyxml2) diff --git a/main/Kconfig.projbuild b/main/Kconfig.projbuild new file mode 100644 index 0000000..9daad6e --- /dev/null +++ b/main/Kconfig.projbuild @@ -0,0 +1,20 @@ +menu "* Project: MQTT Broker Configuration" + + config BROKER_URI + string "Broker URL" + default "mqtts://mqtt.eclipseprojects.io:8883" + help + URL of an mqtt broker which this client connects to. + + config BROKER_USER + string "Broker user" + default "blinky" + help + Username for the mqtt broker. + + config BROKER_PASSWORD + string "Broker password" + help + Password for the mqtt broker. + +endmenu diff --git a/main/http_serv.cpp b/main/http_serv.cpp new file mode 100644 index 0000000..1f2b2bd --- /dev/null +++ b/main/http_serv.cpp @@ -0,0 +1,24 @@ +static const char *TAG = "http_serv"; +#include "esp_log.h" + +#include "http_serv.hpp" + + +httpd_handle_t start_webserver(void) { + ESP_LOGI(TAG, "Starting HTTP server"); + + // Default configuration, EXCEPT for the wildcard URI match + httpd_config_t config = HTTPD_DEFAULT_CONFIG(); + config.stack_size = 4096 * 4; + config.uri_match_fn = httpd_uri_match_wildcard; + + // Start the httpd server + httpd_handle_t server = NULL; + ESP_ERROR_CHECK(httpd_start(&server, &config)); + + return server; +} + +void stop_webserver(httpd_handle_t server) { + if (server) httpd_stop(server); +} \ No newline at end of file diff --git a/main/http_serv.hpp b/main/http_serv.hpp new file mode 100644 index 0000000..f96a9d8 --- /dev/null +++ b/main/http_serv.hpp @@ -0,0 +1,7 @@ +#pragma once + +#include + + +httpd_handle_t start_webserver(void); +void stop_webserver(httpd_handle_t server); \ No newline at end of file diff --git a/main/main.cpp b/main/main.cpp new file mode 100644 index 0000000..7fb5559 --- /dev/null +++ b/main/main.cpp @@ -0,0 +1,115 @@ +static const char *TAG = "blinky"; +#include + +#include +#include + +#include +#include + +#include "wifi.hpp" +#include "http_serv.hpp" +#include "ota.hpp" +#include "mqtt.hpp" + + +// mDNS / NetBIOS + +#include +#include + +static void start_mdns_service(const char *hostname) { + // Initialize mDNS service + ESP_ERROR_CHECK(mdns_init()); + + // Set hostname + mdns_hostname_set(hostname); + + // Set default instance + mdns_instance_name_set("Blinky Lights"); + + // NetBIOS too + netbiosns_init(); + netbiosns_set_name(hostname); +} + + +// Dummy FS + +static void start_filesystem() { + ESP_LOGI(TAG, "Initializing filesystem"); + + esp_vfs_spiffs_conf_t conf = { + .base_path = "/spiffs", + .partition_label = NULL, + .max_files = 5, + .format_if_mount_failed = true + }; + + // Use settings defined above to initialize and mount SPIFFS filesystem. + // Note: esp_vfs_spiffs_register is an all-in-one convenience function. + ESP_ERROR_CHECK(esp_vfs_spiffs_register(&conf)); +} + +#include +#include +#include +#include + +static void log_dir(const std::string &path) { + struct stat st; + int err = stat(path.c_str(), &st); + if (err < 0) { + ESP_LOGE(TAG, "%s %d", path.c_str(), errno); + } else { + ESP_LOGI(TAG, "%s %d", path.c_str(), st.st_size); + } + + DIR *dir; + if ((dir = opendir(path.c_str())) == NULL) { + //ESP_LOGE(TAG, "Failed to open %s", path.c_str()); + return; + } + + struct dirent *de; + while ((de = readdir(dir)) != NULL) { + log_dir(path + "/" + de->d_name); + } + + closedir(dir); +} + + +// Entry Point + +extern "C" void app_main(void) { + // Initialize NVS for WiFi Data + esp_err_t ret = nvs_flash_init(); + if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) { + ESP_ERROR_CHECK(nvs_flash_erase()); + ret = nvs_flash_init(); + } + ESP_ERROR_CHECK(ret); + + // WiFi + config_wifi(); + + // mDNS + start_mdns_service("blinky-jr"); + + // HTTP Server + httpd_handle_t server = start_webserver(); + + // OTA Server + start_ota_serv(server); + + // Dummy Filesystem + start_filesystem(); + log_dir("/spiffs"); + + // TODO: Something useful + esp_mqtt_client_handle_t client = start_mqtt_client(NULL, NULL, NULL); + esp_mqtt_client_subscribe(client, "#", 0); + + // Spin +} diff --git a/main/mqtt.cpp b/main/mqtt.cpp new file mode 100644 index 0000000..42831d6 --- /dev/null +++ b/main/mqtt.cpp @@ -0,0 +1,108 @@ +static const char *TAG = "mqtt"; +#include + +#include "mqtt.hpp" + + +extern const uint8_t broker_pem_start[] asm("_binary_broker_pem_start"); + + +typedef struct { + mqtt_connect_handler connect; + mqtt_message_handler message; + void* data; +} mqtt_client_data; + + +static void mqtt_event_handler(void *handler_args, esp_event_base_t base, int32_t event_id, void *event_data) { + mqtt_client_data *client_data = (mqtt_client_data*)handler_args; + ESP_LOGD(TAG, "Event dispatched from event loop base=%s, event_id=%d", base, event_id); + esp_mqtt_event_handle_t event = (esp_mqtt_event_handle_t)event_data; + esp_mqtt_client_handle_t client = event->client; + int msg_id; + switch ((esp_mqtt_event_id_t)event_id) { + case MQTT_EVENT_CONNECTED: + ESP_LOGI(TAG, "MQTT_EVENT_CONNECTED"); + if (client_data && client_data->connect) { + client_data->connect(client, client_data->data); + } + break; + case MQTT_EVENT_DISCONNECTED: + ESP_LOGI(TAG, "MQTT_EVENT_DISCONNECTED"); + break; + case MQTT_EVENT_SUBSCRIBED: + ESP_LOGI(TAG, "MQTT_EVENT_SUBSCRIBED, msg_id=%d", event->msg_id); + break; + case MQTT_EVENT_UNSUBSCRIBED: + ESP_LOGI(TAG, "MQTT_EVENT_UNSUBSCRIBED, msg_id=%d", event->msg_id); + break; + case MQTT_EVENT_PUBLISHED: + ESP_LOGI(TAG, "MQTT_EVENT_PUBLISHED, msg_id=%d", event->msg_id); + break; + case MQTT_EVENT_DATA: + ESP_LOGI(TAG, "MQTT_EVENT_DATA"); + printf("TOPIC=%.*s\r\n", event->topic_len, event->topic); + printf("DATA=%.*s\r\n", event->data_len, event->data); + if (client_data && client_data->message) { + client_data->message(client, event, client_data->data); + } + break; + case MQTT_EVENT_ERROR: + ESP_LOGI(TAG, "MQTT_EVENT_ERROR"); + if (event->error_handle->error_type == MQTT_ERROR_TYPE_TCP_TRANSPORT) { + ESP_LOGI(TAG, "Last error code reported from esp-tls: 0x%x", event->error_handle->esp_tls_last_esp_err); + ESP_LOGI(TAG, "Last tls stack error number: 0x%x", event->error_handle->esp_tls_stack_err); + ESP_LOGI(TAG, "Last captured errno : %d (%s)", event->error_handle->esp_transport_sock_errno, + strerror(event->error_handle->esp_transport_sock_errno)); + } else if (event->error_handle->error_type == MQTT_ERROR_TYPE_CONNECTION_REFUSED) { + ESP_LOGI(TAG, "Connection refused error: 0x%x", event->error_handle->connect_return_code); + } else { + ESP_LOGW(TAG, "Unknown error type: 0x%x", event->error_handle->error_type); + } + break; +/* + case MQTT_USER_EVENT: + event->data = (char*)handler_args; + break; +*/ + default: + ESP_LOGI(TAG, "Other event id:%d", event->event_id); + break; + } +} + +esp_mqtt_client_handle_t start_mqtt_client( + mqtt_connect_handler connect, + mqtt_message_handler message, + void* data + ) { + ESP_LOGI(TAG, "Starting MQTT client"); + + mqtt_client_data* client_data = (mqtt_client_data*)malloc(sizeof(*client_data)); + client_data->connect = connect; + client_data->message = message; + client_data->data = data; + + const esp_mqtt_client_config_t mqtt_cfg = { + .uri = CONFIG_BROKER_URI, + .cert_pem = (const char *)broker_pem_start, + .username = CONFIG_BROKER_USER, + .password = CONFIG_BROKER_PASSWORD, + }; + + esp_mqtt_client_handle_t client = esp_mqtt_client_init(&mqtt_cfg); + esp_mqtt_client_register_event(client, MQTT_EVENT_ANY, mqtt_event_handler, client_data); + esp_mqtt_client_start(client); + + return client; +} + +void stop_mqtt_client(esp_mqtt_client_handle_t client) { +/* + // Hijack the user event to grab the client data on the heap + esp_mqtt_event_t event; + esp_mqtt_dispatch_custom_event(client, &event); + esp_mqtt_client_stop(client); + free((mqtt_client_data*)event->data) +*/ +} \ No newline at end of file diff --git a/main/mqtt.hpp b/main/mqtt.hpp new file mode 100644 index 0000000..bf7c952 --- /dev/null +++ b/main/mqtt.hpp @@ -0,0 +1,17 @@ +#pragma once + +#include + + +typedef void (*mqtt_connect_handler)( + esp_mqtt_client_handle_t, void* + ); + +typedef void (*mqtt_message_handler)( + esp_mqtt_client_handle_t, esp_mqtt_event_handle_t, void* + ); + +esp_mqtt_client_handle_t start_mqtt_client( + mqtt_connect_handler, mqtt_message_handler, void* + ); +void stop_mqtt_client(esp_mqtt_client_handle_t); \ No newline at end of file diff --git a/main/ota.cpp b/main/ota.cpp new file mode 100644 index 0000000..13cf9a9 --- /dev/null +++ b/main/ota.cpp @@ -0,0 +1,163 @@ +static const char *TAG = "ota"; +#include + +#include +#include + +#include +#include + +#include "ota.hpp" + + +// The real version probably lives somewhere else. Too lazy to find it +template static inline T MIN(T a, T b) { return a < b ? a : b; } + +// TODO: Stick this somewhere useful? +static void reboot_soon_task(void*) { + int delay_s = 1; + ESP_LOGI(TAG, "Rebooting in %d second%s", + delay_s, delay_s == 1 ? "" : "s"); + vTaskDelay(pdMS_TO_TICKS(delay_s * 1000)); + esp_restart(); +} + +static void reboot_soon() { + xTaskCreate( + reboot_soon_task, "reboot_task", + configMINIMAL_STACK_SIZE * 2, // TODO: Choose the best size + NULL, 1, NULL + ); +} + +// HTTP Server Helpers. +// TODO: Share these? +static inline esp_err_t serv_err(httpd_req_t *req, const char *msg) { + ESP_LOGE(TAG, "%s", msg); + return httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, msg); +} + +static inline esp_err_t serv_progress(httpd_req_t *req, int progress) { + ESP_LOGI(TAG, "%d%% complete", progress); + /* + char msg[100]; + snprintf(msg, sizeof(msg), "%d%% complete\n", progress); + httpd_resp_set_status(req, "102 Processing"); + httpd_resp_set_type(req, HTTPD_TYPE_TEXT); + return httpd_resp_send(req, msg, HTTPD_RESP_USE_STRLEN); + */ + return ESP_OK; +} + + +// OTA POST +static esp_err_t ota_post_handler(httpd_req_t *req) +{ + esp_err_t err; + + const esp_partition_t *part = esp_ota_get_next_update_partition(NULL); + if (!part) return serv_err(req, "Could not find update partition"); + + int remaining = req->content_len; + ESP_LOGI(TAG, "Starting OTA: %d bytes", remaining); + + esp_ota_handle_t ota; + err = esp_ota_begin(part, remaining, &ota); + if (err != ESP_OK) { + ESP_LOGE(TAG, "esp_ota_begin: %d", err); + return serv_err(req, "Could not start OTA"); + } + + constexpr int progress_step = 10; + int next_progress = 0; + constexpr size_t CHUNK_MAX = 4096; + char *chunk_buf = new char[CHUNK_MAX]; + while (remaining > 0) { + int chunk_size = MIN((unsigned int)remaining, CHUNK_MAX); + + int nread = httpd_req_recv(req, chunk_buf, chunk_size); + if (nread <= 0) { /* 0 return value indicates connection closed */ + ESP_LOGE(TAG, "httpd_req_recv(%d): %d", chunk_size, nread); + esp_ota_abort(ota); + delete[] chunk_buf; + + /* Check if timeout occurred */ + if (nread == HTTPD_SOCK_ERR_TIMEOUT) { + /* In case of timeout one can choose to retry calling + * httpd_req_recv(), but to keep it simple, here we + * respond with an HTTP 408 (Request Timeout) error */ + httpd_resp_send_408(req); + } + + /* In case of error, returning ESP_FAIL will + * ensure that the underlying socket is closed */ + return ESP_FAIL; + } + + err = esp_ota_write(ota, chunk_buf, nread); + if (err != ESP_OK) { + ESP_LOGE(TAG, "esp_ota_write(%d): %d", nread, err); + esp_ota_abort(ota); + delete[] chunk_buf; + return serv_err(req, "Failed to write OTA chunk"); + } + + remaining -= nread; + + int progress = 100 * (req->content_len - remaining) / req->content_len; + if (progress >= next_progress) { + serv_progress(req, progress); + next_progress += progress_step; + } + } + delete[] chunk_buf; + + ESP_LOGI(TAG, "Finalizing OTA"); + + err = esp_ota_end(ota); + if (err != ESP_OK) { + ESP_LOGE(TAG, "esp_ota_end: %d", err); + esp_ota_abort(ota); + return serv_err(req, "Failed to finalize OTA"); + } + + ESP_LOGI(TAG, "Setting new boot partition"); + err = esp_ota_set_boot_partition(part); + if (err != ESP_OK) { + ESP_LOGE(TAG, "esp_ota_set_boot_partition: %d", err); + return serv_err(req, "Failed to set partition"); + } + + ESP_LOGI(TAG, "OTA Successful"); + httpd_resp_sendstr(req, "OTA Successful\n"); + + reboot_soon(); + + return ESP_FAIL; +} + + +int start_ota_serv(httpd_handle_t server) { + ESP_LOGI(TAG, "Starting OTA server"); + + // Register handlers (move these to components) + httpd_uri_t uri_ota_post = { + .uri = "/ota", + .method = HTTP_POST, + .handler = ota_post_handler, + .user_ctx = NULL + }; + ESP_ERROR_CHECK(httpd_register_uri_handler(server, &uri_ota_post)); + + // mDNS Entry + mdns_service_add(NULL, "_http", "_tcp", 80, NULL, 0); + mdns_service_instance_name_set("_http", "_tcp", "ESP32S2 OTA"); + + return 0; +} + + +int stop_ota_serv() { + // TODO + return -1; +} diff --git a/main/ota.hpp b/main/ota.hpp new file mode 100644 index 0000000..ef126c5 --- /dev/null +++ b/main/ota.hpp @@ -0,0 +1,7 @@ +#pragma once + +#include + + +int start_ota_serv(httpd_handle_t); +int stop_ota_serv(); diff --git a/main/wifi.cpp b/main/wifi.cpp new file mode 100644 index 0000000..c4821b1 --- /dev/null +++ b/main/wifi.cpp @@ -0,0 +1,107 @@ +static const char *TAG = "WiFi"; +#include "esp_log.h" + + +#include "wifi.hpp" + +// Defines SSID and PASS +#include "wifi_cfg.hpp" + +static const int CONFIG_ESP_MAXIMUM_RETRY = 3; + + +#include "esp_wifi.h" +#include "freertos/event_groups.h" + +static EventGroupHandle_t s_wifi_event_group; + +#define WIFI_CONNECTED_BIT BIT0 +#define WIFI_FAIL_BIT BIT1 + +static void event_handler(void* arg, esp_event_base_t event_base, + int32_t event_id, void* event_data) { + static int s_retry_num = 0; + + if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_START) { + esp_wifi_connect(); + + } else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_DISCONNECTED) { + ESP_LOGI(TAG, "Disconnected, retrying connection to AP"); + esp_wifi_connect(); + + } else if (event_base == IP_EVENT && event_id == IP_EVENT_STA_GOT_IP) { + ip_event_got_ip_t* event = (ip_event_got_ip_t*) event_data; + ESP_LOGI(TAG, "Got IP address: " IPSTR, IP2STR(&event->ip_info.ip)); + s_retry_num = 0; + xEventGroupSetBits(s_wifi_event_group, WIFI_CONNECTED_BIT); + } +} + +int config_wifi() { + ESP_LOGI(TAG, "Connecting to %s", SSID); + + s_wifi_event_group = xEventGroupCreate(); + + ESP_ERROR_CHECK(esp_netif_init()); + + ESP_ERROR_CHECK(esp_event_loop_create_default()); + esp_netif_create_default_wifi_sta(); + + wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT(); + ESP_ERROR_CHECK(esp_wifi_init(&cfg)); + + esp_event_handler_instance_t instance_any_id; + esp_event_handler_instance_t instance_got_ip; + ESP_ERROR_CHECK(esp_event_handler_instance_register( + WIFI_EVENT, + ESP_EVENT_ANY_ID, + &event_handler, + NULL, + &instance_any_id + )); + ESP_ERROR_CHECK(esp_event_handler_instance_register( + IP_EVENT, + IP_EVENT_STA_GOT_IP, + &event_handler, + NULL, + &instance_got_ip + )); + + wifi_config_t wifi_config = { + .sta = { + .ssid = SSID, + .password = PASS, + }, + }; + + ESP_ERROR_CHECK( esp_wifi_set_mode(WIFI_MODE_STA) ); + ESP_ERROR_CHECK( esp_wifi_set_config(WIFI_IF_STA, &wifi_config) ); + ESP_ERROR_CHECK( esp_wifi_start() ); + +/* + // Waiting until either the connection is established (WIFI_CONNECTED_BIT) + // or connection failed for the maximum number of re-tries (WIFI_FAIL_BIT). + // The bits are set by event_handler() (see above) + EventBits_t bits = xEventGroupWaitBits( + s_wifi_event_group, + WIFI_CONNECTED_BIT | WIFI_FAIL_BIT, + pdFALSE, + pdFALSE, + portMAX_DELAY + ); + + // xEventGroupWaitBits() returns the bits before the call returned, hence we + // can test which event actually happened. + if (bits & WIFI_CONNECTED_BIT) { + ESP_LOGI(TAG, "Connected to AP: %s", SSID); + return 0; + + } else if (bits & WIFI_FAIL_BIT) { + ESP_LOGE(TAG, "Failed to connect to SSID %s", SSID); + } else { + ESP_LOGE(TAG, "UNEXPECTED EVENT"); + } +*/ + + return 0; +} diff --git a/main/wifi.hpp b/main/wifi.hpp new file mode 100644 index 0000000..8e9a0a0 --- /dev/null +++ b/main/wifi.hpp @@ -0,0 +1,3 @@ +#pragma once + +int config_wifi(); \ No newline at end of file diff --git a/partitions.csv b/partitions.csv new file mode 100644 index 0000000..4b4e4a0 --- /dev/null +++ b/partitions.csv @@ -0,0 +1,8 @@ +# Name, Type, SubType, Offset, Size, Flags +nvs, data, nvs, 0x9000, 0x4000 +otadata, data, ota, , 0x2000 +phy_init, data, phy, , 0x1000 +factory, app, factory, , 1M +ota_0, app, ota_0, , 1M +ota_1, app, ota_1, , 1M +storage, data, spiffs, , 0xF0000