| @@ -0,0 +1,7 @@ | |||
| build | |||
| main/wifi_cfg.hpp | |||
| CMakeFiles | |||
| CMakeCache.txt | |||
| sdkconfig | |||
| sdkconfig.old | |||
| main/broker.pem | |||
| @@ -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) | |||
| @@ -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) | |||
| @@ -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 | |||
| @@ -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); | |||
| } | |||
| @@ -0,0 +1,7 @@ | |||
| #pragma once | |||
| #include <esp_http_server.h> | |||
| httpd_handle_t start_webserver(void); | |||
| void stop_webserver(httpd_handle_t server); | |||
| @@ -0,0 +1,115 @@ | |||
| static const char *TAG = "blinky"; | |||
| #include <esp_log.h> | |||
| #include <freertos/FreeRTOS.h> | |||
| #include <freertos/task.h> | |||
| #include <esp_spiffs.h> | |||
| #include <nvs_flash.h> | |||
| #include "wifi.hpp" | |||
| #include "http_serv.hpp" | |||
| #include "ota.hpp" | |||
| #include "mqtt.hpp" | |||
| // mDNS / NetBIOS | |||
| #include <mdns.h> | |||
| #include <lwip/apps/netbiosns.h> | |||
| 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 <string> | |||
| #include <dirent.h> | |||
| #include <sys/stat.h> | |||
| #include <errno.h> | |||
| 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 | |||
| } | |||
| @@ -0,0 +1,108 @@ | |||
| static const char *TAG = "mqtt"; | |||
| #include <esp_log.h> | |||
| #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) | |||
| */ | |||
| } | |||
| @@ -0,0 +1,17 @@ | |||
| #pragma once | |||
| #include <mqtt_client.h> | |||
| 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); | |||
| @@ -0,0 +1,163 @@ | |||
| static const char *TAG = "ota"; | |||
| #include <esp_log.h> | |||
| #include <mdns.h> | |||
| #include <esp_ota_ops.h> | |||
| #include <freertos/FreeRTOS.h> | |||
| #include <freertos/task.h> | |||
| #include "ota.hpp" | |||
| // The real version probably lives somewhere else. Too lazy to find it | |||
| template <class T> 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; | |||
| } | |||
| @@ -0,0 +1,7 @@ | |||
| #pragma once | |||
| #include <esp_http_server.h> | |||
| int start_ota_serv(httpd_handle_t); | |||
| int stop_ota_serv(); | |||
| @@ -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; | |||
| } | |||
| @@ -0,0 +1,3 @@ | |||
| #pragma once | |||
| int config_wifi(); | |||
| @@ -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 | |||