. // === Início de: main/main.c === // === Início de: main/main.c === #include #include #include #include "freertos/FreeRTOS.h" #include "freertos/task.h" #include "freertos/event_groups.h" #include "esp_log.h" #include "esp_err.h" #include "esp_event.h" #include "esp_netif.h" #include "esp_spiffs.h" #include "esp_system.h" #include "nvs_flash.h" #include "driver/gpio.h" #include "network.h" #include "board_config.h" #include "logger.h" #include "rest_main.h" #include "peripherals.h" #include "protocols.h" #include "evse_manager.h" #include "evse_core.h" #include "auth.h" #include "loadbalancer.h" #include "meter_manager.h" #include "buzzer.h" #include "evse_link.h" #include "ocpp.h" #include "led.h" #include "scheduler.h" #define AP_CONNECTION_TIMEOUT 120000 #define RESET_HOLD_TIME 30000 #define DEBOUNCE_TIME_MS 50 #define PRESS_BIT BIT0 #define RELEASED_BIT BIT1 static const char *TAG = "app_main"; static TaskHandle_t user_input_task = NULL; static TickType_t press_tick = 0; static volatile TickType_t last_interrupt_tick = 0; static bool pressed = false; // // File system (SPIFFS) init and info // static void fs_info(esp_vfs_spiffs_conf_t *conf) { size_t total = 0, used = 0; esp_err_t ret = esp_spiffs_info(conf->partition_label, &total, &used); if (ret == ESP_OK) ESP_LOGI(TAG, "Partition %s: total: %d, used: %d", conf->partition_label, total, used); else ESP_LOGE(TAG, "Failed to get SPIFFS info: %s", esp_err_to_name(ret)); } static void fs_init(void) { esp_vfs_spiffs_conf_t cfg_conf = { .base_path = "/cfg", .partition_label = "cfg", .max_files = 1, .format_if_mount_failed = false}; esp_vfs_spiffs_conf_t data_conf = { .base_path = "/data", .partition_label = "data", .max_files = 5, .format_if_mount_failed = true}; ESP_ERROR_CHECK(esp_vfs_spiffs_register(&cfg_conf)); ESP_ERROR_CHECK(esp_vfs_spiffs_register(&data_conf)); fs_info(&cfg_conf); fs_info(&data_conf); } // // Wi-Fi event monitoring task // static void wifi_event_task_func(void *param) { EventBits_t mode_bits; for (;;) { mode_bits = xEventGroupWaitBits( wifi_event_group, WIFI_AP_MODE_BIT | WIFI_STA_MODE_BIT, pdFALSE, pdFALSE, portMAX_DELAY); if (mode_bits & WIFI_AP_MODE_BIT) { if (xEventGroupWaitBits( wifi_event_group, WIFI_AP_CONNECTED_BIT, pdFALSE, pdFALSE, pdMS_TO_TICKS(AP_CONNECTION_TIMEOUT)) & WIFI_AP_CONNECTED_BIT) { xEventGroupWaitBits( wifi_event_group, WIFI_AP_DISCONNECTED_BIT, pdFALSE, pdFALSE, portMAX_DELAY); } else { if (xEventGroupGetBits(wifi_event_group) & WIFI_AP_MODE_BIT) { // wifi_ap_stop(); } } } else if (mode_bits & WIFI_STA_MODE_BIT) { xEventGroupWaitBits( wifi_event_group, WIFI_STA_DISCONNECTED_BIT, pdFALSE, pdFALSE, portMAX_DELAY); } } } // // Button press handler // static void handle_button_press(void) { if (!(xEventGroupGetBits(wifi_event_group) & WIFI_AP_MODE_BIT)) { ESP_LOGI(TAG, "Starting Wi-Fi AP mode"); wifi_ap_start(); } } // Task to handle button press/release notifications static void user_input_task_func(void *param) { uint32_t notification; for (;;) { if (xTaskNotifyWait( 0, UINT32_MAX, ¬ification, portMAX_DELAY)) { if (notification & PRESS_BIT) { press_tick = xTaskGetTickCount(); pressed = true; ESP_LOGI(TAG, "Button Pressed"); handle_button_press(); } if ((notification & RELEASED_BIT) && pressed) { pressed = false; TickType_t held = xTaskGetTickCount() - press_tick; ESP_LOGI(TAG, "Button Released (held %u ms)", (unsigned)pdTICKS_TO_MS(held)); if (held >= pdMS_TO_TICKS(RESET_HOLD_TIME)) { ESP_LOGW(TAG, "Long press: erasing NVS + reboot"); nvs_flash_erase(); esp_restart(); } } } } } // ISR for button GPIO interrupt (active-low) static void IRAM_ATTR button_isr_handler(void *arg) { BaseType_t higher_task_woken = pdFALSE; TickType_t now = xTaskGetTickCountFromISR(); if (now - last_interrupt_tick < pdMS_TO_TICKS(DEBOUNCE_TIME_MS)) { return; } last_interrupt_tick = now; if (user_input_task == NULL) { return; } int level = gpio_get_level(board_config.button_wifi_gpio); if (level == 0) { xTaskNotifyFromISR( user_input_task, PRESS_BIT, eSetBits, &higher_task_woken); } else { xTaskNotifyFromISR( user_input_task, RELEASED_BIT, eSetBits, &higher_task_woken); } if (higher_task_woken) { portYIELD_FROM_ISR(); } } static void button_init(void) { gpio_config_t conf = { .pin_bit_mask = BIT64(board_config.button_wifi_gpio), .mode = GPIO_MODE_INPUT, .pull_down_en = GPIO_PULLDOWN_DISABLE, .pull_up_en = GPIO_PULLUP_ENABLE, .intr_type = GPIO_INTR_ANYEDGE}; ESP_ERROR_CHECK(gpio_config(&conf)); ESP_ERROR_CHECK(gpio_isr_handler_add(board_config.button_wifi_gpio, button_isr_handler, NULL)); } // // Inicialização dos módulos do sistema (SEM botão) // static void init_modules(void) { peripherals_init(); led_init(); wifi_ini(); buzzer_init(); ESP_ERROR_CHECK(rest_server_init("/data")); protocols_init(); evse_manager_init(); evse_init(); auth_init(); loadbalancer_init(); meter_manager_init(); meter_manager_start(); evse_link_init(); ocpp_start(); scheduler_init(); } // // Função principal do firmware // void app_main(void) { logger_init(); esp_log_set_vprintf(logger_vprintf); esp_reset_reason_t reason = esp_reset_reason(); ESP_LOGI(TAG, "Reset reason: %d", reason); esp_err_t ret = nvs_flash_init(); if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) { ESP_LOGW(TAG, "Erasing NVS flash"); ESP_ERROR_CHECK(nvs_flash_erase()); ret = nvs_flash_init(); } ESP_ERROR_CHECK(ret); fs_init(); ESP_ERROR_CHECK(esp_netif_init()); ESP_ERROR_CHECK(esp_event_loop_create_default()); ESP_ERROR_CHECK(gpio_install_isr_service(0)); board_config_load(); // 1) cria a task que recebe notificações do botão xTaskCreate(user_input_task_func, "user_input_task", 4 * 1024, NULL, 3, &user_input_task); // 2) agora é seguro registrar ISR do botão button_init(); // 3) inicia o resto do sistema init_modules(); // 4) tasks auxiliares xTaskCreate(wifi_event_task_func, "wifi_event_task", 8 * 1024, NULL, 3, NULL); } // === Fim de: main/main.c === // === Fim de: main/main.c === // === Início de: components/rest_api/src/ocpp_api.c === // ========================= // ocpp_api.c // ========================= #include "ocpp.h" #include "esp_log.h" #include "esp_err.h" #include "esp_http_server.h" #include "cJSON.h" #include static const char *TAG = "ocpp_api"; static esp_err_t ocpp_get_status_handler(httpd_req_t *req) { ESP_LOGD(TAG, "GET /api/v1/ocpp"); httpd_resp_set_type(req, "application/json"); char server[64] = {0}; char charge_id[64] = {0}; ocpp_get_server(server); ocpp_get_charge_id(charge_id); cJSON *status = cJSON_CreateObject(); cJSON_AddBoolToObject(status, "connected", ocpp_is_connected()); cJSON_AddStringToObject(status, "server", server); cJSON_AddStringToObject(status, "charge_id", charge_id); char *str = cJSON_PrintUnformatted(status); httpd_resp_sendstr(req, str); free(str); cJSON_Delete(status); return ESP_OK; } static esp_err_t ocpp_get_config_handler(httpd_req_t *req) { ESP_LOGD(TAG, "GET /api/v1/config/ocpp"); httpd_resp_set_type(req, "application/json"); char server[64] = {0}; char charge_id[64] = {0}; bool enabled = ocpp_get_enabled(); ocpp_get_server(server); ocpp_get_charge_id(charge_id); cJSON *json = cJSON_CreateObject(); cJSON_AddBoolToObject(json, "enabled", enabled); cJSON_AddStringToObject(json, "url", server); cJSON_AddStringToObject(json, "chargeBoxId", charge_id); char *str = cJSON_PrintUnformatted(json); httpd_resp_sendstr(req, str); free(str); cJSON_Delete(json); return ESP_OK; } static esp_err_t ocpp_post_config_handler(httpd_req_t *req) { ESP_LOGD(TAG, "POST /api/v1/config/ocpp"); char buf[512]; int len = httpd_req_recv(req, buf, sizeof(buf) - 1); if (len <= 0) { httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Empty body"); return ESP_FAIL; } buf[len] = '\0'; cJSON *json = cJSON_Parse(buf); if (!json) { httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Invalid JSON"); return ESP_FAIL; } cJSON *enabled = cJSON_GetObjectItem(json, "enabled"); if (cJSON_IsBool(enabled)) { ocpp_set_enabled(cJSON_IsTrue(enabled)); } cJSON *url = cJSON_GetObjectItem(json, "url"); if (cJSON_IsString(url)) { ocpp_set_server(url->valuestring); } cJSON *id = cJSON_GetObjectItem(json, "chargeBoxId"); if (cJSON_IsString(id)) { ocpp_set_charge_id(id->valuestring); } cJSON_Delete(json); httpd_resp_sendstr(req, "OCPP config atualizada com sucesso"); return ESP_OK; } void register_ocpp_handlers(httpd_handle_t server, void *ctx) { httpd_uri_t status_uri = { .uri = "/api/v1/ocpp", .method = HTTP_GET, .handler = ocpp_get_status_handler, .user_ctx = ctx }; httpd_register_uri_handler(server, &status_uri); httpd_uri_t get_uri = { .uri = "/api/v1/config/ocpp", .method = HTTP_GET, .handler = ocpp_get_config_handler, .user_ctx = ctx }; httpd_register_uri_handler(server, &get_uri); httpd_uri_t post_uri = { .uri = "/api/v1/config/ocpp", .method = HTTP_POST, .handler = ocpp_post_config_handler, .user_ctx = ctx }; httpd_register_uri_handler(server, &post_uri); } // === Fim de: components/rest_api/src/ocpp_api.c === // === Início de: components/rest_api/src/static_file_api.c === #include "static_file_api.h" #include "esp_log.h" #include #include #include "esp_vfs.h" static const char *TAG = "static_file_api"; #define FILE_PATH_MAX (ESP_VFS_PATH_MAX + 128) #define SCRATCH_BUFSIZE (10240) typedef struct rest_server_context { char base_path[ESP_VFS_PATH_MAX + 1]; char scratch[SCRATCH_BUFSIZE]; } rest_server_context_t; #define CHECK_FILE_EXTENSION(filename, ext) \ (strcasecmp(&filename[strlen(filename) - strlen(ext)], ext) == 0) static esp_err_t set_content_type_from_file(httpd_req_t *req, const char *filepath) { const char *type = "text/plain"; if (CHECK_FILE_EXTENSION(filepath, ".html")) type = "text/html"; else if (CHECK_FILE_EXTENSION(filepath, ".js")) type = "application/javascript"; else if (CHECK_FILE_EXTENSION(filepath, ".css")) type = "text/css"; else if (CHECK_FILE_EXTENSION(filepath, ".png")) type = "image/png"; else if (CHECK_FILE_EXTENSION(filepath, ".ico")) type = "image/x-icon"; else if (CHECK_FILE_EXTENSION(filepath, ".svg")) type = "image/svg+xml"; return httpd_resp_set_type(req, type); } static esp_err_t static_get_handler(httpd_req_t *req) { char filepath[FILE_PATH_MAX]; rest_server_context_t *ctx = (rest_server_context_t *) req->user_ctx; strlcpy(filepath, ctx->base_path, sizeof(filepath)); if (req->uri[strlen(req->uri) - 1] == '/') { strlcat(filepath, "/index.html", sizeof(filepath)); } else { strlcat(filepath, req->uri, sizeof(filepath)); } int fd = open(filepath, O_RDONLY, 0); if (fd == -1) { // fallback para /index.html (SPA) ESP_LOGW(TAG, "Arquivo não encontrado: %s. Tentando index.html", filepath); strlcpy(filepath, ctx->base_path, sizeof(filepath)); strlcat(filepath, "/index.html", sizeof(filepath)); fd = open(filepath, O_RDONLY, 0); if (fd == -1) { httpd_resp_send_err(req, HTTPD_404_NOT_FOUND, "Arquivo não encontrado"); return ESP_FAIL; } } set_content_type_from_file(req, filepath); char *chunk = ctx->scratch; ssize_t read_bytes; do { read_bytes = read(fd, chunk, SCRATCH_BUFSIZE); if (read_bytes == -1) { ESP_LOGE(TAG, "Erro lendo arquivo: %s", filepath); close(fd); httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Erro ao ler arquivo"); return ESP_FAIL; } else if (read_bytes > 0) { if (httpd_resp_send_chunk(req, chunk, read_bytes) != ESP_OK) { close(fd); httpd_resp_sendstr_chunk(req, NULL); httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Erro ao enviar arquivo"); return ESP_FAIL; } } } while (read_bytes > 0); close(fd); httpd_resp_send_chunk(req, NULL, 0); return ESP_OK; } void register_static_file_handlers(httpd_handle_t server, void *ctx) { httpd_uri_t uri = { .uri = "/*", .method = HTTP_GET, .handler = static_get_handler, .user_ctx = ctx }; httpd_register_uri_handler(server, &uri); } // === Fim de: components/rest_api/src/static_file_api.c === // === Início de: components/rest_api/src/evse_link_config_api.c === #include "evse_link.h" #include "evse_link_config_api.h" // new header for these handlers #include "esp_log.h" #include "cJSON.h" static const char *TAG = "link_config_api"; // GET /api/v1/config/link static esp_err_t link_config_get_handler(httpd_req_t *req) { bool enabled = evse_link_is_enabled(); uint8_t mode = evse_link_get_mode(); // 0=MASTER,1=SLAVE uint8_t self_id = evse_link_get_self_id(); ESP_LOGI(TAG, "GET link config: enabled=%d mode=%u id=%u", enabled, mode, self_id); httpd_resp_set_type(req, "application/json"); cJSON *root = cJSON_CreateObject(); cJSON_AddBoolToObject (root, "linkEnabled", enabled); cJSON_AddStringToObject(root, "linkMode", mode == EVSE_LINK_MODE_MASTER ? "MASTER" : "SLAVE"); cJSON_AddNumberToObject(root, "linkSelfId", self_id); char *s = cJSON_Print(root); httpd_resp_sendstr(req, s); ESP_LOGI(TAG, " payload: %s", s); free(s); cJSON_Delete(root); return ESP_OK; } // POST /api/v1/config/link static esp_err_t link_config_post_handler(httpd_req_t *req) { char buf[256]; int len = httpd_req_recv(req, buf, sizeof(buf)-1); if (len <= 0) { httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Empty body"); return ESP_FAIL; } buf[len] = '\0'; ESP_LOGI(TAG, "POST link config: %s", buf); cJSON *json = cJSON_Parse(buf); if (!json) { httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Invalid JSON"); return ESP_FAIL; } // linkEnabled cJSON *j_en = cJSON_GetObjectItem(json, "linkEnabled"); if (j_en && cJSON_IsBool(j_en)) { evse_link_set_enabled(cJSON_IsTrue(j_en)); ESP_LOGI(TAG, " set enabled = %d", cJSON_IsTrue(j_en)); } // linkMode cJSON *j_md = cJSON_GetObjectItem(json, "linkMode"); if (j_md && cJSON_IsString(j_md)) { const char *m = j_md->valuestring; if (strcmp(m, "MASTER") == 0) { evse_link_set_mode(EVSE_LINK_MODE_MASTER); } else if (strcmp(m, "SLAVE") == 0) { evse_link_set_mode(EVSE_LINK_MODE_SLAVE); } else { cJSON_Delete(json); httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Invalid linkMode (must be MASTER or SLAVE)"); return ESP_FAIL; } ESP_LOGI(TAG, " set mode = %s", m); } // linkSelfId cJSON *j_id = cJSON_GetObjectItem(json, "linkSelfId"); if (j_id && cJSON_IsNumber(j_id)) { int id = j_id->valueint; if (id < 0 || id > 254) { cJSON_Delete(json); httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Invalid linkSelfId (0–254)"); return ESP_FAIL; } evse_link_set_self_id((uint8_t)id); ESP_LOGI(TAG, " set self_id = %d", id); } cJSON_Delete(json); httpd_resp_sendstr(req, "Link settings updated"); return ESP_OK; } void register_link_config_handlers(httpd_handle_t server, void *ctx) { httpd_uri_t get = { .uri = "/api/v1/config/link", .method = HTTP_GET, .handler = link_config_get_handler, .user_ctx = ctx }; httpd_register_uri_handler(server, &get); httpd_uri_t post = { .uri = "/api/v1/config/link", .method = HTTP_POST, .handler = link_config_post_handler, .user_ctx = ctx }; httpd_register_uri_handler(server, &post); } // === Fim de: components/rest_api/src/evse_link_config_api.c === // === Início de: components/rest_api/src/scheduler_settings_api.c === #include "scheduler_settings_api.h" #include "scheduler.h" #include "scheduler_types.h" #include "esp_log.h" #include "esp_http_server.h" #include "cJSON.h" #include #include #include // sscanf, snprintf static const char *TAG = "scheduler_api"; /* ========================= * Helpers HH:MM <-> minutos * ========================= */ static bool parse_hhmm(const char *s, uint16_t *out_min) { if (!s || !out_min) return false; // formato esperado: "HH:MM" int h = 0, m = 0; if (sscanf(s, "%d:%d", &h, &m) != 2) { return false; } if (h < 0 || h > 23 || m < 0 || m > 59) { return false; } *out_min = (uint16_t)(h * 60 + m); return true; } static void format_hhmm(uint16_t minutes, char *buf, size_t buf_sz) { if (!buf || buf_sz < 6) return; minutes %= (24 * 60); int h = minutes / 60; int m = minutes % 60; snprintf(buf, buf_sz, "%02d:%02d", h, m); } /* ========================= * GET /api/v1/config/scheduler * ========================= */ static esp_err_t scheduler_config_get_handler(httpd_req_t *req) { ESP_LOGI(TAG, "GET /api/v1/config/scheduler"); httpd_resp_set_type(req, "application/json"); sched_config_t cfg = scheduler_get_config(); bool allowed_now = scheduler_is_allowed_now(); cJSON *root = cJSON_CreateObject(); if (!root) { httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "cJSON alloc failed"); return ESP_FAIL; } cJSON_AddBoolToObject(root, "enabled", cfg.enabled); cJSON_AddStringToObject(root, "mode", sched_mode_to_str(cfg.mode)); char buf[8]; format_hhmm(cfg.start_min, buf, sizeof(buf)); cJSON_AddStringToObject(root, "startTime", buf); format_hhmm(cfg.end_min, buf, sizeof(buf)); cJSON_AddStringToObject(root, "endTime", buf); cJSON_AddBoolToObject(root, "allowedNow", allowed_now); char *json_str = cJSON_PrintUnformatted(root); if (!json_str) { cJSON_Delete(root); httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "cJSON print failed"); return ESP_FAIL; } httpd_resp_sendstr(req, json_str); free(json_str); cJSON_Delete(root); return ESP_OK; } /* ========================= * POST /api/v1/config/scheduler * ========================= */ static esp_err_t scheduler_config_post_handler(httpd_req_t *req) { ESP_LOGI(TAG, "POST /api/v1/config/scheduler"); // NOTA: para payloads pequenos 512 bytes chega; se quiseres robustez total, // usa req->content_len e faz um loop com httpd_req_recv. char buf[512]; int len = httpd_req_recv(req, buf, sizeof(buf) - 1); if (len <= 0) { ESP_LOGE(TAG, "Empty body / recv error"); httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Empty body"); return ESP_FAIL; } buf[len] = '\0'; ESP_LOGI(TAG, "Body: %s", buf); cJSON *json = cJSON_Parse(buf); if (!json) { ESP_LOGE(TAG, "Invalid JSON"); httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Invalid JSON"); return ESP_FAIL; } // Começa a partir da config atual sched_config_t cfg = scheduler_get_config(); // enabled cJSON *j_enabled = cJSON_GetObjectItem(json, "enabled"); if (cJSON_IsBool(j_enabled)) { cfg.enabled = cJSON_IsTrue(j_enabled); ESP_LOGI(TAG, " enabled = %d", cfg.enabled); } // mode cJSON *j_mode = cJSON_GetObjectItem(json, "mode"); if (cJSON_IsString(j_mode) && j_mode->valuestring) { sched_mode_t m; if (!sched_mode_from_str(j_mode->valuestring, &m)) { ESP_LOGW(TAG, "Invalid mode: %s", j_mode->valuestring); cJSON_Delete(json); httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Invalid mode (use: disabled|simple|weekly)"); return ESP_FAIL; } cfg.mode = m; ESP_LOGI(TAG, " mode = %s", sched_mode_to_str(cfg.mode)); } // startTime (string "HH:MM") cJSON *j_start = cJSON_GetObjectItem(json, "startTime"); if (cJSON_IsString(j_start) && j_start->valuestring) { uint16_t minutes = 0; if (!parse_hhmm(j_start->valuestring, &minutes)) { ESP_LOGW(TAG, "Invalid startTime: %s", j_start->valuestring); cJSON_Delete(json); httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Invalid startTime (use HH:MM)"); return ESP_FAIL; } cfg.start_min = minutes; ESP_LOGI(TAG, " start_min = %u", (unsigned)cfg.start_min); } // endTime (string "HH:MM") cJSON *j_end = cJSON_GetObjectItem(json, "endTime"); if (cJSON_IsString(j_end) && j_end->valuestring) { uint16_t minutes = 0; if (!parse_hhmm(j_end->valuestring, &minutes)) { ESP_LOGW(TAG, "Invalid endTime: %s", j_end->valuestring); cJSON_Delete(json); httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Invalid endTime (use HH:MM)"); return ESP_FAIL; } cfg.end_min = minutes; ESP_LOGI(TAG, " end_min = %u", (unsigned)cfg.end_min); } // (Opcional) validações extra: // exemplo: impedir janela vazia quando ativo /* if (cfg.enabled && cfg.mode != SCHED_MODE_DISABLED && cfg.start_min == cfg.end_min) { cJSON_Delete(json); httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "startTime and endTime cannot be equal"); return ESP_FAIL; } */ // Aplica config no módulo scheduler (ele trata de NVS + eventos) scheduler_set_config(&cfg); cJSON_Delete(json); httpd_resp_sendstr(req, "Scheduler config atualizada com sucesso"); return ESP_OK; } /* ========================= * Registo dos handlers * ========================= */ void register_scheduler_settings_handlers(httpd_handle_t server, void *ctx) { httpd_uri_t get_uri = { .uri = "/api/v1/config/scheduler", .method = HTTP_GET, .handler = scheduler_config_get_handler, .user_ctx = ctx}; httpd_register_uri_handler(server, &get_uri); httpd_uri_t post_uri = { .uri = "/api/v1/config/scheduler", .method = HTTP_POST, .handler = scheduler_config_post_handler, .user_ctx = ctx}; httpd_register_uri_handler(server, &post_uri); ESP_LOGI(TAG, "Scheduler REST handlers registered"); } // === Fim de: components/rest_api/src/scheduler_settings_api.c === // === Início de: components/rest_api/src/meters_settings_api.c === #include "meters_settings_api.h" #include "meter_manager.h" // Atualizado para usar o novo manager #include "meter_events.h" #include "esp_event.h" #include "esp_log.h" #include "cJSON.h" #include "freertos/semphr.h" #include "esp_timer.h" #include static const char *TAG = "meters_settings_api"; typedef struct { meter_event_data_t last_grid; meter_event_data_t last_evse; bool has_grid; bool has_evse; int64_t ts_grid_us; int64_t ts_evse_us; SemaphoreHandle_t mtx; } meters_cache_t; static meters_cache_t g_cache = {0}; static void cache_init_once(void) { if (!g_cache.mtx) { g_cache.mtx = xSemaphoreCreateMutex(); } } static void copy_event_locked(const meter_event_data_t *src) { if (!src) return; if (strcmp(src->source ? src->source : "", "EVSE") == 0) { g_cache.last_evse = *src; g_cache.has_evse = true; g_cache.ts_evse_us = esp_timer_get_time(); } else { // Default trate como GRID g_cache.last_grid = *src; g_cache.has_grid = true; g_cache.ts_grid_us = esp_timer_get_time(); } } // ---------- Event handler (atualiza cache) ---------- static void meters_event_handler(void *arg, esp_event_base_t base, int32_t id, void *data) { if (base != METER_EVENT || id != METER_EVENT_DATA_READY || data == NULL) return; cache_init_once(); if (xSemaphoreTake(g_cache.mtx, pdMS_TO_TICKS(10)) == pdTRUE) { copy_event_locked((const meter_event_data_t *)data); xSemaphoreGive(g_cache.mtx); } } // ---------- Helpers JSON ---------- static cJSON *meter_event_to_json(const meter_event_data_t *m, int64_t ts_us) { cJSON *root = cJSON_CreateObject(); cJSON_AddStringToObject(root, "source", m->source ? m->source : "GRID"); cJSON_AddNumberToObject(root, "frequency", m->frequency); cJSON_AddNumberToObject(root, "powerFactor", m->power_factor); cJSON_AddNumberToObject(root, "totalEnergy", m->total_energy); // timestamp relativo (segundos desde boot) e em micros cJSON_AddNumberToObject(root, "timestampUs", (double)ts_us); // arrays cJSON *vr = cJSON_CreateArray(); for (int i = 0; i < 3; i++) cJSON_AddItemToArray(vr, cJSON_CreateNumber(m->vrms[i])); cJSON *ir = cJSON_CreateArray(); for (int i = 0; i < 3; i++) cJSON_AddItemToArray(ir, cJSON_CreateNumber(m->irms[i])); cJSON *pw = cJSON_CreateArray(); for (int i = 0; i < 3; i++) cJSON_AddItemToArray(pw, cJSON_CreateNumber(m->watt[i])); cJSON_AddItemToObject(root, "vrms", vr); cJSON_AddItemToObject(root, "irms", ir); cJSON_AddItemToObject(root, "watt", pw); return root; } // ---------- HTTP GET /api/v1/meters/live ---------- static esp_err_t meters_live_get_handler(httpd_req_t *req) { cache_init_once(); char query[64] = {0}; char source[16] = {0}; bool want_grid = true, want_evse = true; if (httpd_req_get_url_query_str(req, query, sizeof(query)) == ESP_OK) { if (httpd_query_key_value(query, "source", source, sizeof(source)) == ESP_OK) { if (strcasecmp(source, "GRID") == 0) { want_grid = true; want_evse = false; } else if (strcasecmp(source, "EVSE") == 0) { want_grid = false; want_evse = true; } } } cJSON *out = cJSON_CreateObject(); cJSON *arr = cJSON_CreateArray(); if (xSemaphoreTake(g_cache.mtx, pdMS_TO_TICKS(50)) == pdTRUE) { if (want_grid && g_cache.has_grid) { cJSON_AddItemToArray(arr, meter_event_to_json(&g_cache.last_grid, g_cache.ts_grid_us)); } if (want_evse && g_cache.has_evse) { cJSON_AddItemToArray(arr, meter_event_to_json(&g_cache.last_evse, g_cache.ts_evse_us)); } xSemaphoreGive(g_cache.mtx); } cJSON_AddItemToObject(out, "meters", arr); httpd_resp_set_type(req, "application/json"); char *s = cJSON_PrintUnformatted(out); httpd_resp_sendstr(req, s ? s : "{\"meters\":[]}"); if (s) free(s); cJSON_Delete(out); return ESP_OK; } // ---------- Registro ---------- void register_meters_data_handlers(httpd_handle_t server, void *ctx) { // registra event handler para receber amostras ESP_ERROR_CHECK(esp_event_handler_register(METER_EVENT, METER_EVENT_DATA_READY, meters_event_handler, NULL)); // endpoint GET httpd_uri_t live = { .uri = "/api/v1/meters/live", .method = HTTP_GET, .handler = meters_live_get_handler, .user_ctx = ctx}; httpd_register_uri_handler(server, &live); ESP_LOGI(TAG, "REST /api/v1/meters/live registrado"); } // Função para recuperar as configurações dos contadores static esp_err_t meters_config_get_handler(httpd_req_t *req) { ESP_LOGI(TAG, "Received GET request for /api/v1/config/meters"); httpd_resp_set_type(req, "application/json"); cJSON *config = cJSON_CreateObject(); // Recuperando as configurações dos contadores meter_type_t gridmeterType = meter_manager_grid_get_model(); meter_type_t evsemeterType = meter_manager_evse_get_model(); ESP_LOGI(TAG, "Grid meter type: %s", meter_type_to_str(gridmeterType)); ESP_LOGI(TAG, "EVSE meter type: %s", meter_type_to_str(evsemeterType)); // Adicionando os tipos de contadores ao objeto JSON cJSON_AddStringToObject(config, "gridmeter", meter_type_to_str(gridmeterType)); cJSON_AddStringToObject(config, "evsemeter", meter_type_to_str(evsemeterType)); // Convertendo o objeto JSON para uma string const char *json_str = cJSON_Print(config); ESP_LOGI(TAG, "Returning meters config: %s", json_str); httpd_resp_sendstr(req, json_str); // Liberação da memória free((void *)json_str); cJSON_Delete(config); return ESP_OK; } // Função para atualizar as configurações dos contadores static esp_err_t meters_config_post_handler(httpd_req_t *req) { ESP_LOGI(TAG, "Received POST request for /api/v1/config/meters"); char buf[512]; int len = httpd_req_recv(req, buf, sizeof(buf) - 1); if (len <= 0) { ESP_LOGE(TAG, "Received empty body in POST request"); httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Empty body"); return ESP_FAIL; } buf[len] = '\0'; // Garantir que a string está terminada ESP_LOGI(TAG, "Received POST data: %s", buf); cJSON *json = cJSON_Parse(buf); if (!json) { ESP_LOGE(TAG, "Failed to parse JSON data"); // Resposta detalhada de erro httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Invalid JSON format"); return ESP_FAIL; } // Atualizando os contadores cJSON *gridmeter = cJSON_GetObjectItem(json, "gridmeter"); if (gridmeter) { meter_type_t gridType = string_to_meter_type(gridmeter->valuestring); // Usando a função string_to_meter_type ESP_LOGI(TAG, "Updating grid meter type to: %s", gridmeter->valuestring); meter_manager_grid_set_model(gridType); } cJSON *evsemeter = cJSON_GetObjectItem(json, "evsemeter"); if (evsemeter) { meter_type_t evseType = string_to_meter_type(evsemeter->valuestring); // Usando a função string_to_meter_type ESP_LOGI(TAG, "Updating EVSE meter type to: %s", evsemeter->valuestring); meter_manager_evse_set_model(evseType); } cJSON_Delete(json); httpd_resp_sendstr(req, "Meters updated successfully"); ESP_LOGI(TAG, "Meters configuration updated successfully"); return ESP_OK; } // Registrando os manipuladores de URI para os contadores void register_meters_settings_handlers(httpd_handle_t server, void *ctx) { ESP_LOGD(TAG, "Registering URI handlers for meters settings"); // URI para o método GET httpd_uri_t meters_get_uri = { .uri = "/api/v1/config/meters", .method = HTTP_GET, .handler = meters_config_get_handler, .user_ctx = ctx}; ESP_LOGD(TAG, "Registering GET handler for /api/v1/config/meters"); httpd_register_uri_handler(server, &meters_get_uri); // URI para o método POST httpd_uri_t meters_post_uri = { .uri = "/api/v1/config/meters", .method = HTTP_POST, .handler = meters_config_post_handler, .user_ctx = ctx}; ESP_LOGD(TAG, "Registering POST handler for /api/v1/config/meters"); httpd_register_uri_handler(server, &meters_post_uri); } // === Fim de: components/rest_api/src/meters_settings_api.c === // === Início de: components/rest_api/src/rest_main.c === #include "rest_main.h" #include "evse_settings_api.h" #include "meters_settings_api.h" #include "loadbalancing_settings_api.h" #include "evse_link_config_api.h" #include "network_api.h" #include "ocpp_api.h" #include "auth_api.h" #include "dashboard_api.h" #include "scheduler_settings_api.h" #include "static_file_api.h" #include "esp_log.h" static const char *TAG = "rest_main"; esp_err_t rest_server_init(const char *base_path) { ESP_LOGI(TAG, "Initializing REST API with base path: %s", base_path); rest_server_context_t *ctx = calloc(1, sizeof(rest_server_context_t)); if (!ctx) { ESP_LOGE(TAG, "Failed to allocate memory for REST context"); return ESP_ERR_NO_MEM; } strlcpy(ctx->base_path, base_path, sizeof(ctx->base_path)); httpd_config_t config = HTTPD_DEFAULT_CONFIG(); config.uri_match_fn = httpd_uri_match_wildcard; config.max_uri_handlers = 32; httpd_handle_t server = NULL; esp_err_t err = httpd_start(&server, &config); if (err != ESP_OK) { ESP_LOGE(TAG, "Failed to start HTTP server: %s", esp_err_to_name(err)); free(ctx); return err; } ESP_LOGI(TAG, "HTTP server started successfully"); // Register endpoint groups register_evse_settings_handlers(server, ctx); // Apenas chamando a função sem comparação register_network_handlers(server, ctx); // Apenas chamando a função sem comparação register_ocpp_handlers(server, ctx); // Apenas chamando a função sem comparação register_auth_handlers(server, ctx); // Apenas chamando a função sem comparação register_dashboard_handlers(server, ctx); // Apenas chamando a função sem comparação register_meters_settings_handlers(server, ctx); // Apenas chamando a função sem comparação register_loadbalancing_settings_handlers(server, ctx); // Apenas chamando a função sem comparação register_link_config_handlers(server, ctx); register_meters_data_handlers(server, ctx); register_scheduler_settings_handlers(server, ctx); register_static_file_handlers(server, ctx); // Apenas chamando a função sem comparação ESP_LOGI(TAG, "All REST API endpoint groups registered successfully"); return ESP_OK; } // === Fim de: components/rest_api/src/rest_main.c === // === Início de: components/rest_api/src/network_api.c === // ========================= // network_api.c // ========================= #include "network_api.h" #include "esp_log.h" #include "cJSON.h" #include "network.h" #include "mqtt.h" static const char *TAG = "network_api"; typedef struct { bool enabled; char ssid[33]; char password[65]; } wifi_task_data_t; static void wifi_apply_config_task(void *param) { wifi_task_data_t *data = (wifi_task_data_t *)param; ESP_LOGI("wifi_task", "Applying Wi-Fi config in background task"); wifi_set_config(data->enabled, data->ssid, data->password); free(data); vTaskDelete(NULL); } static esp_err_t wifi_get_handler(httpd_req_t *req) { ESP_LOGI(TAG, "Handling GET /api/v1/config/wifi"); httpd_resp_set_type(req, "application/json"); // Obter dados da NVS via wifi.c bool enabled = wifi_get_enabled(); char ssid[33] = {0}; char password[65] = {0}; wifi_get_ssid(ssid); wifi_get_password(password); // Criar JSON cJSON *json = cJSON_CreateObject(); cJSON_AddBoolToObject(json, "enabled", enabled); cJSON_AddStringToObject(json, "ssid", ssid); cJSON_AddStringToObject(json, "password", password); // Enviar resposta char *response = cJSON_Print(json); httpd_resp_sendstr(req, response); // Limpeza free(response); cJSON_Delete(json); return ESP_OK; } static esp_err_t wifi_post_handler(httpd_req_t *req) { ESP_LOGI(TAG, "Handling POST /api/v1/config/wifi"); char buf[512]; int len = httpd_req_recv(req, buf, sizeof(buf) - 1); if (len <= 0) return ESP_FAIL; buf[len] = '\0'; cJSON *json = cJSON_Parse(buf); if (!json) return ESP_FAIL; // Valores padrão bool enabled = false; const char *ssid = NULL; const char *password = NULL; cJSON *j_enabled = cJSON_GetObjectItem(json, "enabled"); if (cJSON_IsBool(j_enabled)) enabled = j_enabled->valueint; cJSON *j_ssid = cJSON_GetObjectItem(json, "ssid"); if (cJSON_IsString(j_ssid)) ssid = j_ssid->valuestring; cJSON *j_password = cJSON_GetObjectItem(json, "password"); if (cJSON_IsString(j_password)) password = j_password->valuestring; // Enviar resposta antes de alterar Wi-Fi httpd_resp_sendstr(req, "Wi-Fi config atualizada com sucesso"); // Alocar struct para passar para a task wifi_task_data_t *task_data = malloc(sizeof(wifi_task_data_t)); if (!task_data) { cJSON_Delete(json); ESP_LOGE(TAG, "Memory allocation failed for Wi-Fi task"); return ESP_ERR_NO_MEM; } task_data->enabled = enabled; strncpy(task_data->ssid, ssid ? ssid : "", sizeof(task_data->ssid)); strncpy(task_data->password, password ? password : "", sizeof(task_data->password)); // Criar task normal com função C xTaskCreate( wifi_apply_config_task, "wifi_config_task", 4096, task_data, 3, NULL ); cJSON_Delete(json); return ESP_OK; } static esp_err_t config_mqtt_get_handler(httpd_req_t *req) { ESP_LOGI(TAG, "Handling GET /api/v1/config/mqtt"); httpd_resp_set_type(req, "application/json"); bool enabled = mqtt_get_enabled(); char server[64] = {0}; char base_topic[32] = {0}; char username[32] = {0}; char password[64] = {0}; uint16_t periodicity = mqtt_get_periodicity(); mqtt_get_server(server); mqtt_get_base_topic(base_topic); mqtt_get_user(username); mqtt_get_password(password); ESP_LOGI(TAG, "MQTT Config:"); ESP_LOGI(TAG, " Enabled: %s", enabled ? "true" : "false"); ESP_LOGI(TAG, " Server: %s", server); ESP_LOGI(TAG, " Topic: %s", base_topic); ESP_LOGI(TAG, " Username: %s", username); ESP_LOGI(TAG, " Password: %s", password); ESP_LOGI(TAG, " Periodicity: %d", periodicity); cJSON *config = cJSON_CreateObject(); cJSON_AddBoolToObject(config, "enabled", enabled); cJSON_AddStringToObject(config, "host", server); cJSON_AddNumberToObject(config, "port", 1883); cJSON_AddStringToObject(config, "username", username); cJSON_AddStringToObject(config, "password", password); cJSON_AddStringToObject(config, "topic", base_topic); cJSON_AddNumberToObject(config, "periodicity", periodicity); const char *config_str = cJSON_Print(config); httpd_resp_sendstr(req, config_str); free((void *)config_str); cJSON_Delete(config); return ESP_OK; } static esp_err_t config_mqtt_post_handler(httpd_req_t *req) { ESP_LOGI(TAG, "Handling POST /api/v1/config/mqtt"); char buf[512]; int len = httpd_req_recv(req, buf, sizeof(buf) - 1); if (len <= 0) { ESP_LOGE(TAG, "Failed to read request body"); httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Invalid request body"); return ESP_FAIL; } buf[len] = '\0'; ESP_LOGI(TAG, "Received JSON: %s", buf); cJSON *json = cJSON_Parse(buf); if (!json) { ESP_LOGE(TAG, "Invalid JSON format"); httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Invalid JSON"); return ESP_FAIL; } bool enabled = false; const char *host = NULL, *topic = NULL, *username = NULL, *password = NULL; int periodicity = 30; if (cJSON_IsBool(cJSON_GetObjectItem(json, "enabled"))) enabled = cJSON_GetObjectItem(json, "enabled")->valueint; cJSON *j_host = cJSON_GetObjectItem(json, "host"); if (cJSON_IsString(j_host)) host = j_host->valuestring; cJSON *j_topic = cJSON_GetObjectItem(json, "topic"); if (cJSON_IsString(j_topic)) topic = j_topic->valuestring; cJSON *j_user = cJSON_GetObjectItem(json, "username"); if (cJSON_IsString(j_user)) username = j_user->valuestring; cJSON *j_pass = cJSON_GetObjectItem(json, "password"); if (cJSON_IsString(j_pass)) password = j_pass->valuestring; cJSON *j_periodicity = cJSON_GetObjectItem(json, "periodicity"); if (cJSON_IsNumber(j_periodicity)) periodicity = j_periodicity->valueint; ESP_LOGI(TAG, "Applying MQTT config:"); ESP_LOGI(TAG, " Enabled: %s", enabled ? "true" : "false"); ESP_LOGI(TAG, " Host: %s", host); ESP_LOGI(TAG, " Topic: %s", topic); ESP_LOGI(TAG, " Username: %s", username); ESP_LOGI(TAG, " Password: %s", password); ESP_LOGI(TAG, " Periodicity: %d", periodicity); esp_err_t err = mqtt_set_config(enabled, host, topic, username, password, periodicity); if (err != ESP_OK) { ESP_LOGE(TAG, "Failed to apply MQTT config (code %d)", err); httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Failed to apply config"); cJSON_Delete(json); return ESP_FAIL; } httpd_resp_sendstr(req, "Configuração MQTT atualizada com sucesso"); cJSON_Delete(json); return ESP_OK; } void register_network_handlers(httpd_handle_t server, void *ctx) { httpd_uri_t wifi_get = { .uri = "/api/v1/config/wifi", .method = HTTP_GET, .handler = wifi_get_handler, .user_ctx = ctx }; httpd_register_uri_handler(server, &wifi_get); httpd_uri_t wifi_post = { .uri = "/api/v1/config/wifi", .method = HTTP_POST, .handler = wifi_post_handler, .user_ctx = ctx }; httpd_register_uri_handler(server, &wifi_post); // URI handler for getting MQTT config httpd_uri_t config_mqtt_get_uri = { .uri = "/api/v1/config/mqtt", .method = HTTP_GET, .handler = config_mqtt_get_handler, .user_ctx = ctx }; httpd_register_uri_handler(server, &config_mqtt_get_uri); // URI handler for posting MQTT config httpd_uri_t config_mqtt_post_uri = { .uri = "/api/v1/config/mqtt", .method = HTTP_POST, .handler = config_mqtt_post_handler, .user_ctx = ctx }; httpd_register_uri_handler(server, &config_mqtt_post_uri); } // === Fim de: components/rest_api/src/network_api.c === // === Início de: components/rest_api/src/dashboard_api.c === #include "dashboard_api.h" #include "esp_log.h" #include "cJSON.h" #include "evse_api.h" #include "evse_error.h" #include "evse_config.h" #include "evse_limits.h" static const char *TAG = "dashboard_api"; static esp_err_t dashboard_get_handler(httpd_req_t *req) { httpd_resp_set_type(req, "application/json"); // Cria o objeto JSON principal do dashboard cJSON *dashboard = cJSON_CreateObject(); // Status do sistema evse_state_t state = evse_get_state(); cJSON_AddStringToObject(dashboard, "status", evse_state_to_str(state)); // Carregador - informação do carregador 1 (adapte conforme necessário) cJSON *chargers = cJSON_CreateArray(); cJSON *charger1 = cJSON_CreateObject(); cJSON_AddNumberToObject(charger1, "id", 1); cJSON_AddStringToObject(charger1, "status", evse_state_to_str(state)); cJSON_AddNumberToObject(charger1, "current", evse_get_runtime_charging_current()); cJSON_AddNumberToObject(charger1, "maxCurrent", evse_get_max_charging_current()); // Calcular a potência com base na corrente (considerando 230V) int power = (evse_get_runtime_charging_current()) * 230; cJSON_AddNumberToObject(charger1, "power", power); cJSON_AddItemToArray(chargers, charger1); cJSON_AddItemToObject(dashboard, "chargers", chargers); // Consumo e tempo de carregamento cJSON_AddNumberToObject(dashboard, "energyConsumed", evse_get_consumption_limit()); cJSON_AddNumberToObject(dashboard, "chargingTime", evse_get_charging_time_limit()); // Alertas cJSON *alerts = cJSON_CreateArray(); if (evse_is_limit_reached()) { cJSON_AddItemToArray(alerts, cJSON_CreateString("Limite de consumo atingido.")); } if (!evse_config_is_available()) { cJSON_AddItemToArray(alerts, cJSON_CreateString("Estação indisponível.")); } if (!evse_config_is_enabled()) { cJSON_AddItemToArray(alerts, cJSON_CreateString("EVSE desativado.")); } cJSON_AddItemToObject(dashboard, "alerts", alerts); // Erros uint32_t error_bits = evse_get_error(); cJSON *errors = cJSON_CreateArray(); if (error_bits & EVSE_ERR_DIODE_SHORT_BIT) cJSON_AddItemToArray(errors, cJSON_CreateString("Diodo curto-circuitado")); if (error_bits & EVSE_ERR_LOCK_FAULT_BIT) cJSON_AddItemToArray(errors, cJSON_CreateString("Falha no travamento")); if (error_bits & EVSE_ERR_UNLOCK_FAULT_BIT) cJSON_AddItemToArray(errors, cJSON_CreateString("Falha no destravamento")); if (error_bits & EVSE_ERR_RCM_SELFTEST_FAULT_BIT) cJSON_AddItemToArray(errors, cJSON_CreateString("Falha no autoteste do RCM")); if (error_bits & EVSE_ERR_RCM_TRIGGERED_BIT) cJSON_AddItemToArray(errors, cJSON_CreateString("RCM disparado")); if (error_bits & EVSE_ERR_TEMPERATURE_HIGH_BIT) cJSON_AddItemToArray(errors, cJSON_CreateString("Temperatura elevada")); if (error_bits & EVSE_ERR_PILOT_FAULT_BIT) cJSON_AddItemToArray(errors, cJSON_CreateString("Erro no sinal piloto")); if (error_bits & EVSE_ERR_TEMPERATURE_FAULT_BIT) cJSON_AddItemToArray(errors, cJSON_CreateString("Falha no sensor de temperatura")); cJSON_AddItemToObject(dashboard, "errors", errors); // Enviar resposta JSON const char *json_str = cJSON_Print(dashboard); httpd_resp_sendstr(req, json_str); // Liberar memória free((void *)json_str); cJSON_Delete(dashboard); return ESP_OK; } void register_dashboard_handlers(httpd_handle_t server, void *ctx) { httpd_uri_t uri = { .uri = "/api/v1/dashboard", .method = HTTP_GET, .handler = dashboard_get_handler, .user_ctx = ctx }; httpd_register_uri_handler(server, &uri); } // === Fim de: components/rest_api/src/dashboard_api.c === // === Início de: components/rest_api/src/auth_api.c === // ========================= // auth_api.c (nova interface por modo) // ========================= #include "auth_api.h" #include "auth.h" #include "esp_log.h" #include "cJSON.h" #include static const char *TAG = "auth_api"; // ================================= // Dummy user storage (static list) // ================================= static struct { char username[128]; } users[10] = { {"admin"}, {"user1"} }; static int num_users = 2; // ================================= // Helpers // ================================= static void send_json(httpd_req_t *req, cJSON *json) { char *str = cJSON_PrintUnformatted(json); httpd_resp_set_type(req, "application/json"); httpd_resp_sendstr(req, str ? str : "{}"); if (str) free(str); cJSON_Delete(json); } static esp_err_t recv_body(httpd_req_t *req, char *buf, size_t buf_sz, int *out_len) { int remaining = req->content_len; int received = 0; if (remaining <= 0) { *out_len = 0; return ESP_OK; } while (remaining > 0 && received < (int)(buf_sz - 1)) { int chunk = remaining; int space = (int)(buf_sz - 1 - received); if (chunk > space) chunk = space; int ret = httpd_req_recv(req, buf + received, chunk); if (ret <= 0) return ESP_FAIL; received += ret; remaining -= ret; } buf[received] = '\0'; *out_len = received; return ESP_OK; } // ================================= // Auth Mode (NEW API) // ================================= static esp_err_t auth_mode_get_handler(httpd_req_t *req) { auth_mode_t mode = auth_get_mode(); cJSON *json = cJSON_CreateObject(); cJSON_AddStringToObject(json, "mode", auth_mode_to_str(mode)); send_json(req, json); return ESP_OK; } static esp_err_t auth_mode_post_handler(httpd_req_t *req) { char buf[256]; int len = 0; if (recv_body(req, buf, sizeof(buf), &len) != ESP_OK) { httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Erro ao receber dados"); return ESP_FAIL; } cJSON *json = cJSON_ParseWithLength(buf, len); if (!json) { httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "JSON inválido"); return ESP_FAIL; } cJSON *mode_js = cJSON_GetObjectItem(json, "mode"); if (!cJSON_IsString(mode_js) || mode_js->valuestring == NULL) { cJSON_Delete(json); httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Campo 'mode' inválido ou ausente"); return ESP_FAIL; } auth_mode_t mode; if (!auth_mode_from_str(mode_js->valuestring, &mode)) { cJSON_Delete(json); httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Valor de 'mode' inválido (use: open|local|ocpp)"); return ESP_FAIL; } auth_set_mode(mode); cJSON_Delete(json); cJSON *resp = cJSON_CreateObject(); cJSON_AddStringToObject(resp, "mode", auth_mode_to_str(mode)); send_json(req, resp); return ESP_OK; } // ================================= /* Users (mock) */ // ================================= static esp_err_t users_get_handler(httpd_req_t *req) { cJSON *root = cJSON_CreateObject(); cJSON *list = cJSON_CreateArray(); for (int i = 0; i < num_users; ++i) { cJSON *u = cJSON_CreateObject(); cJSON_AddStringToObject(u, "username", users[i].username); cJSON_AddItemToArray(list, u); } cJSON_AddItemToObject(root, "users", list); send_json(req, root); return ESP_OK; } static esp_err_t users_post_handler(httpd_req_t *req) { char buf[128]; int len = 0; if (recv_body(req, buf, sizeof(buf), &len) != ESP_OK || len <= 0) { httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Body vazio"); return ESP_FAIL; } if (num_users < 10) { strlcpy(users[num_users].username, buf, sizeof(users[num_users].username)); num_users++; httpd_resp_sendstr(req, "Usuário adicionado com sucesso"); return ESP_OK; } else { httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Limite de usuários atingido"); return ESP_FAIL; } } static esp_err_t users_delete_handler(httpd_req_t *req) { char query[128]; if (httpd_req_get_url_query_str(req, query, sizeof(query)) == ESP_OK) { char username[128]; if (httpd_query_key_value(query, "username", username, sizeof(username)) == ESP_OK) { for (int i = 0; i < num_users; i++) { if (strcmp(users[i].username, username) == 0) { for (int j = i; j < num_users - 1; j++) { users[j] = users[j + 1]; } num_users--; httpd_resp_sendstr(req, "Usuário removido com sucesso"); return ESP_OK; } } } } httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Usuário não encontrado"); return ESP_FAIL; } // ================================= // Tags (apenas úteis no modo LOCAL) // ================================= static esp_err_t tags_get_handler(httpd_req_t *req) { cJSON *root = cJSON_CreateObject(); cJSON *list = cJSON_CreateArray(); int count = auth_get_tag_count(); for (int i = 0; i < count; i++) { const char *tag = auth_get_tag_by_index(i); if (tag) cJSON_AddItemToArray(list, cJSON_CreateString(tag)); } cJSON_AddItemToObject(root, "tags", list); send_json(req, root); return ESP_OK; } static esp_err_t tags_delete_handler(httpd_req_t *req) { char query[128]; if (httpd_req_get_url_query_str(req, query, sizeof(query)) == ESP_OK) { char tag[AUTH_TAG_MAX_LEN]; if (httpd_query_key_value(query, "tag", tag, sizeof(tag)) == ESP_OK) { if (auth_remove_tag(tag)) { httpd_resp_sendstr(req, "Tag removida com sucesso"); return ESP_OK; } } } httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Tag não encontrada ou inválida"); return ESP_FAIL; } static esp_err_t tags_register_handler(httpd_req_t *req) { if (auth_get_mode() != AUTH_MODE_LOCAL_RFID) { httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Registo de tags disponível apenas no modo LOCAL"); return ESP_FAIL; } auth_wait_for_tag_registration(); httpd_resp_sendstr(req, "Modo de registro de tag ativado"); return ESP_OK; } // ================================= // Register All REST Endpoints // ================================= void register_auth_handlers(httpd_handle_t server, void *ctx) { // Auth mode httpd_register_uri_handler(server, &(httpd_uri_t){ .uri = "/api/v1/config/auth-mode", .method = HTTP_GET, .handler = auth_mode_get_handler, .user_ctx = ctx }); httpd_register_uri_handler(server, &(httpd_uri_t){ .uri = "/api/v1/config/auth-mode", .method = HTTP_POST, .handler = auth_mode_post_handler, .user_ctx = ctx }); // Users (mock) httpd_register_uri_handler(server, &(httpd_uri_t){ .uri = "/api/v1/config/users", .method = HTTP_GET, .handler = users_get_handler, .user_ctx = ctx }); httpd_register_uri_handler(server, &(httpd_uri_t){ .uri = "/api/v1/config/users", .method = HTTP_POST, .handler = users_post_handler, .user_ctx = ctx }); httpd_register_uri_handler(server, &(httpd_uri_t){ .uri = "/api/v1/config/users", .method = HTTP_DELETE, .handler = users_delete_handler, .user_ctx = ctx }); // Tags httpd_register_uri_handler(server, &(httpd_uri_t){ .uri = "/api/v1/config/tags", .method = HTTP_GET, .handler = tags_get_handler, .user_ctx = ctx }); httpd_register_uri_handler(server, &(httpd_uri_t){ .uri = "/api/v1/config/tags", .method = HTTP_DELETE, .handler = tags_delete_handler, .user_ctx = ctx }); httpd_register_uri_handler(server, &(httpd_uri_t){ .uri = "/api/v1/config/tags/register", .method = HTTP_POST, .handler = tags_register_handler, .user_ctx = ctx }); } // === Fim de: components/rest_api/src/auth_api.c === // === Início de: components/rest_api/src/loadbalancing_settings_api.c === #include "loadbalancing_settings_api.h" #include "loadbalancer.h" #include "esp_log.h" #include "cJSON.h" static const char *TAG = "loadbalancing_settings_api"; // GET Handler: Retorna configurações atuais de load balancing static esp_err_t loadbalancing_config_get_handler(httpd_req_t *req) { bool enabled = loadbalancer_is_enabled(); uint8_t currentLimit = load_balancing_get_max_grid_current(); ESP_LOGI(TAG, "Fetching load balancing settings: enabled = %d, currentLimit = %u", enabled, currentLimit); httpd_resp_set_type(req, "application/json"); cJSON *config = cJSON_CreateObject(); cJSON_AddBoolToObject(config, "loadBalancingEnabled", enabled); cJSON_AddNumberToObject(config, "loadBalancingCurrentLimit", currentLimit); const char *json_str = cJSON_Print(config); httpd_resp_sendstr(req, json_str); ESP_LOGI(TAG, "Returned config: %s", json_str); free((void *)json_str); cJSON_Delete(config); return ESP_OK; } // POST Handler: Atualiza configurações de load balancing static esp_err_t loadbalancing_config_post_handler(httpd_req_t *req) { char buf[512]; int len = httpd_req_recv(req, buf, sizeof(buf) - 1); if (len <= 0) { ESP_LOGE(TAG, "Received empty POST body"); httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Empty body"); return ESP_FAIL; } buf[len] = '\0'; ESP_LOGI(TAG, "Received POST data: %s", buf); cJSON *json = cJSON_Parse(buf); if (!json) { ESP_LOGE(TAG, "Invalid JSON"); httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Invalid JSON"); return ESP_FAIL; } // Atualizar estado habilitado cJSON *enabled_item = cJSON_GetObjectItem(json, "loadBalancingEnabled"); if (enabled_item && cJSON_IsBool(enabled_item)) { bool isEnabled = cJSON_IsTrue(enabled_item); loadbalancer_set_enabled(isEnabled); ESP_LOGI(TAG, "Updated loadBalancingEnabled to: %d", isEnabled); } // Atualizar limite de corrente cJSON *limit_item = cJSON_GetObjectItem(json, "loadBalancingCurrentLimit"); if (limit_item && cJSON_IsNumber(limit_item)) { uint8_t currentLimit = (uint8_t)limit_item->valuedouble; // Validar intervalo if (currentLimit < 6 || currentLimit > 100) { ESP_LOGW(TAG, "Rejected invalid currentLimit: %d", currentLimit); cJSON_Delete(json); httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Invalid currentLimit (must be 6-100)"); return ESP_FAIL; } esp_err_t err = load_balancing_set_max_grid_current(currentLimit); if (err != ESP_OK) { ESP_LOGE(TAG, "Failed to save currentLimit: %s", esp_err_to_name(err)); cJSON_Delete(json); httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Failed to save setting"); return ESP_FAIL; } ESP_LOGI(TAG, "Updated loadBalancingCurrentLimit to: %d", currentLimit); } cJSON_Delete(json); httpd_resp_sendstr(req, "Load balancing settings updated successfully"); return ESP_OK; } // Registro dos handlers na API HTTP void register_loadbalancing_settings_handlers(httpd_handle_t server, void *ctx) { // GET httpd_uri_t get_uri = { .uri = "/api/v1/config/loadbalancing", .method = HTTP_GET, .handler = loadbalancing_config_get_handler, .user_ctx = ctx }; httpd_register_uri_handler(server, &get_uri); // POST httpd_uri_t post_uri = { .uri = "/api/v1/config/loadbalancing", .method = HTTP_POST, .handler = loadbalancing_config_post_handler, .user_ctx = ctx }; httpd_register_uri_handler(server, &post_uri); } // === Fim de: components/rest_api/src/loadbalancing_settings_api.c === // === Início de: components/rest_api/src/evse_settings_api.c === // ========================= // evse_settings_api.c // ========================= #include "evse_settings_api.h" #include "evse_api.h" #include "evse_config.h" #include "esp_log.h" #include "cJSON.h" static const char *TAG = "evse_settings_api"; static esp_err_t config_settings_get_handler(httpd_req_t *req) { httpd_resp_set_type(req, "application/json"); cJSON *config = cJSON_CreateObject(); cJSON_AddNumberToObject(config, "currentLimit", evse_get_max_charging_current()); cJSON_AddNumberToObject(config, "temperatureLimit", evse_get_temp_threshold()); const char *json_str = cJSON_Print(config); httpd_resp_sendstr(req, json_str); free((void *)json_str); cJSON_Delete(config); return ESP_OK; } static esp_err_t config_settings_post_handler(httpd_req_t *req) { char buf[512]; int len = httpd_req_recv(req, buf, sizeof(buf) - 1); if (len <= 0) { httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Empty body"); return ESP_FAIL; } buf[len] = '\0'; cJSON *json = cJSON_Parse(buf); if (!json) { httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Invalid JSON"); return ESP_FAIL; } cJSON *current = cJSON_GetObjectItem(json, "currentLimit"); if (current) evse_set_max_charging_current(current->valueint); cJSON *temp = cJSON_GetObjectItem(json, "temperatureLimit"); if (temp) evse_set_temp_threshold(temp->valueint); cJSON_Delete(json); httpd_resp_sendstr(req, "Configurações atualizadas com sucesso"); return ESP_OK; } void register_evse_settings_handlers(httpd_handle_t server, void *ctx) { httpd_uri_t get_uri = { .uri = "/api/v1/config/settings", .method = HTTP_GET, .handler = config_settings_get_handler, .user_ctx = ctx }; httpd_register_uri_handler(server, &get_uri); httpd_uri_t post_uri = { .uri = "/api/v1/config/settings", .method = HTTP_POST, .handler = config_settings_post_handler, .user_ctx = ctx }; httpd_register_uri_handler(server, &post_uri); } // === Fim de: components/rest_api/src/evse_settings_api.c === // === Início de: components/rest_api/include/dashboard_api.h === #pragma once #ifdef __cplusplus extern "C" { #endif #include "esp_http_server.h" /** * @brief Registra o handler da dashboard (status geral do sistema) */ void register_dashboard_handlers(httpd_handle_t server, void *ctx); #ifdef __cplusplus } #endif // === Fim de: components/rest_api/include/dashboard_api.h === // === Início de: components/rest_api/include/scheduler_settings_api.h === // ========================= // scheduler_settings_api.h // ========================= #pragma once #ifdef __cplusplus extern "C" { #endif #include "esp_http_server.h" /** * @brief Registra os handlers de configuração do scheduler * * Endpoints: * GET /api/v1/config/scheduler * POST /api/v1/config/scheduler */ void register_scheduler_settings_handlers(httpd_handle_t server, void *ctx); #ifdef __cplusplus } #endif // === Fim de: components/rest_api/include/scheduler_settings_api.h === // === Início de: components/rest_api/include/static_file_api.h === #pragma once #ifdef __cplusplus extern "C" { #endif #include "esp_http_server.h" /** * @brief Registra o handler para servir arquivos estáticos da web (SPA) */ void register_static_file_handlers(httpd_handle_t server, void *ctx); #ifdef __cplusplus } #endif // === Fim de: components/rest_api/include/static_file_api.h === // === Início de: components/rest_api/include/network_api.h === #pragma once #ifdef __cplusplus extern "C" { #endif #include "esp_http_server.h" /** * @brief Registra os handlers de configuração Wi-Fi e MQTT */ void register_network_handlers(httpd_handle_t server, void *ctx); #ifdef __cplusplus } #endif // === Fim de: components/rest_api/include/network_api.h === // === Início de: components/rest_api/include/auth_api.h === #pragma once #ifdef __cplusplus extern "C" { #endif #include "esp_http_server.h" /** * @brief Registra os handlers de autenticação e gerenciamento de usuários */ void register_auth_handlers(httpd_handle_t server, void *ctx); #ifdef __cplusplus } #endif // === Fim de: components/rest_api/include/auth_api.h === // === Início de: components/rest_api/include/loadbalancing_settings_api.h === // ========================= // loadbalancing_settings_api.h // ========================= #ifndef LOADBALANCING_SETTINGS_API_H #define LOADBALANCING_SETTINGS_API_H #include "esp_err.h" #include "esp_http_server.h" // Função para registrar os manipuladores de URI para as configurações de load balancing e solar void register_loadbalancing_settings_handlers(httpd_handle_t server, void *ctx); #endif // LOADBALANCING_SETTINGS_API_H // === Fim de: components/rest_api/include/loadbalancing_settings_api.h === // === Início de: components/rest_api/include/rest_main.h === #pragma once #include #include #define SCRATCH_BUFSIZE (10240) typedef struct rest_server_context { char base_path[ESP_VFS_PATH_MAX + 1]; char scratch[SCRATCH_BUFSIZE]; } rest_server_context_t; esp_err_t rest_server_init(const char *base_path); // === Fim de: components/rest_api/include/rest_main.h === // === Início de: components/rest_api/include/meters_settings_api.h === // ========================= // meters_settings_api.h // ========================= #ifndef METERS_SETTINGS_API_H #define METERS_SETTINGS_API_H #include "esp_err.h" #include "esp_http_server.h" // Função para registrar os manipuladores de URI para as configurações dos contadores void register_meters_settings_handlers(httpd_handle_t server, void *ctx); /** * Exponha dados do meter via REST. * Endpoints: * GET /api/v1/meters/live -> dados mais recentes (GRID e/ou EVSE) * GET /api/v1/meters/live?source=GRID|EVSE */ void register_meters_data_handlers(httpd_handle_t server, void *ctx); #endif // METERS_SETTINGS_API_H // === Fim de: components/rest_api/include/meters_settings_api.h === // === Início de: components/rest_api/include/ocpp_api.h === #pragma once #ifdef __cplusplus extern "C" { #endif #include "esp_http_server.h" /** * @brief Registra os handlers da configuração e status do OCPP */ void register_ocpp_handlers(httpd_handle_t server, void *ctx); #ifdef __cplusplus } #endif // === Fim de: components/rest_api/include/ocpp_api.h === // === Início de: components/rest_api/include/evse_link_config_api.h === // ========================= // evse_link_config_api.h // ========================= #ifndef EVSE_LINK_CONFIG_API_H #define EVSE_LINK_CONFIG_API_H #include "esp_err.h" #include "esp_http_server.h" #ifdef __cplusplus extern "C" { #endif /** * @brief Registra os manipuladores HTTP para configuração do EVSE-Link. * * Isso adiciona endpoints GET e POST em /api/v1/config/link * para inspecionar e atualizar as configurações de link * (habilitado, modo, self ID). * * @param server Handle do servidor HTTP * @param ctx Contexto do usuário passado aos handlers */ void register_link_config_handlers(httpd_handle_t server, void *ctx); #ifdef __cplusplus } #endif #endif // EVSE_LINK_CONFIG_API_H // === Fim de: components/rest_api/include/evse_link_config_api.h === // === Início de: components/rest_api/include/evse_settings_api.h === #pragma once #ifdef __cplusplus extern "C" { #endif #include "esp_http_server.h" /** * @brief Registra os handlers de configuração elétrica e limites de carregamento */ void register_evse_settings_handlers(httpd_handle_t server, void *ctx); #ifdef __cplusplus } #endif // === Fim de: components/rest_api/include/evse_settings_api.h ===