// Wi-Fi station with exponential-backoff reconnect and queued event POSTs. // HTTP requests run in their own task so networking cannot block audio. #include #include #include "esp_crt_bundle.h" #include "esp_event.h" #include "esp_http_client.h" #include "esp_log.h" #include "esp_netif.h" #include "esp_timer.h" #include "esp_wifi.h" #include "freertos/FreeRTOS.h" #include "freertos/event_groups.h" #include "freertos/queue.h" #include "freertos/task.h" #include "notifier.h" static const char *TAG = "notify"; #define WIFI_CONNECTED_BIT BIT0 #define NET_TASK_STACK 8192 #define NET_TASK_PRIO 5 #define BACKOFF_MIN_MS 1000 #define BACKOFF_MAX_MS 60000 #define EVENT_BODY_SIZE 512 typedef struct { uint32_t delay_ms; } event_msg_t; static QueueHandle_t s_q; static EventGroupHandle_t s_eg; static esp_timer_handle_t s_reconnect_timer; static uint32_t s_backoff_ms = BACKOFF_MIN_MS; static notifier_stats_t s_stats; static void reconnect_cb(void *arg) { esp_err_t err = esp_wifi_connect(); if (err != ESP_OK && err != ESP_ERR_WIFI_CONN) { ESP_LOGW(TAG, "esp_wifi_connect: %s", esp_err_to_name(err)); } } static void wifi_event_handler(void *arg, esp_event_base_t base, int32_t id, void *data) { if (base == WIFI_EVENT && id == WIFI_EVENT_STA_START) { esp_wifi_connect(); } else if (base == WIFI_EVENT && id == WIFI_EVENT_STA_DISCONNECTED) { xEventGroupClearBits(s_eg, WIFI_CONNECTED_BIT); s_stats.wifi_connected = false; ESP_LOGW(TAG, "Wi-Fi disconnected, retry in %lu ms", (unsigned long)s_backoff_ms); esp_timer_stop(s_reconnect_timer); esp_timer_start_once(s_reconnect_timer, (uint64_t)s_backoff_ms * 1000ULL); s_backoff_ms = s_backoff_ms * 2 > BACKOFF_MAX_MS ? BACKOFF_MAX_MS : s_backoff_ms * 2; } else if (base == IP_EVENT && id == IP_EVENT_STA_GOT_IP) { ip_event_got_ip_t *ev = (ip_event_got_ip_t *)data; snprintf(s_stats.ip, sizeof(s_stats.ip), IPSTR, IP2STR(&ev->ip_info.ip)); s_stats.wifi_connected = true; s_backoff_ms = BACKOFF_MIN_MS; xEventGroupSetBits(s_eg, WIFI_CONNECTED_BIT); ESP_LOGI(TAG, "Wi-Fi up, IP %s", s_stats.ip); } } static bool make_event_body(uint32_t delay_ms, char *body, size_t body_size) { int n = snprintf(body, body_size, "{\"delay_ms\":%lu,\"secret\":\"", (unsigned long)delay_ms); if (n < 0 || (size_t)n >= body_size) return false; size_t used = (size_t)n; for (const unsigned char *p = (const unsigned char *)CONFIG_BUZZ_EVENT_SECRET; *p; p++) { char escaped[7]; const char *text = (const char *)p; size_t len = 1; if (*p == '"' || *p == '\\') { escaped[0] = '\\'; escaped[1] = (char)*p; text = escaped; len = 2; } else if (*p < 0x20) { snprintf(escaped, sizeof(escaped), "\\u%04x", *p); text = escaped; len = 6; } if (len >= body_size - used) return false; memcpy(body + used, text, len); used += len; } if (body_size - used < 3) return false; body[used++] = '"'; body[used++] = '}'; body[used] = '\0'; return true; } // One queue item produces at most one HTTP request: there is deliberately no retry. static bool post_event(uint32_t delay_ms) { EventBits_t bits = xEventGroupWaitBits(s_eg, WIFI_CONNECTED_BIT, pdFALSE, pdTRUE, pdMS_TO_TICKS(15000)); if (!(bits & WIFI_CONNECTED_BIT)) { ESP_LOGE(TAG, "Wi-Fi unavailable; event was not sent"); return false; } char body[EVENT_BODY_SIZE]; if (!make_event_body(delay_ms, body, sizeof(body))) { ESP_LOGE(TAG, "event body exceeds buffer"); return false; } const char *url = "http://192.168.0.154:3000/event/buzz"; esp_http_client_config_t cfg = { .url = url, .method = HTTP_METHOD_POST, .timeout_ms = 10000, .disable_auto_redirect = true, .crt_bundle_attach = esp_crt_bundle_attach, }; esp_http_client_handle_t c = esp_http_client_init(&cfg); if (!c) { ESP_LOGE(TAG, "could not create HTTP client"); return false; } esp_err_t err = esp_http_client_set_header(c, "Content-Type", "application/json"); if (err == ESP_OK) err = esp_http_client_set_post_field(c, body, (int)strlen(body)); if (err != ESP_OK) { ESP_LOGE(TAG, "configure event POST: %s", esp_err_to_name(err)); esp_http_client_cleanup(c); return false; } err = esp_http_client_perform(c); int status = err == ESP_OK ? esp_http_client_get_status_code(c) : 0; esp_http_client_cleanup(c); if (err != ESP_OK) { ESP_LOGE(TAG, "event POST failed: %s", esp_err_to_name(err)); return false; } if (status < 200 || status >= 300) { ESP_LOGE(TAG, "event endpoint returned HTTP %d", status); return false; } ESP_LOGI(TAG, "sent buzz duration %lu ms (HTTP %d)", (unsigned long)delay_ms, status); return true; } static void net_task(void *arg) { event_msg_t msg; for (;;) { if (xQueueReceive(s_q, &msg, portMAX_DELAY) == pdTRUE) { if (post_event(msg.delay_ms)) s_stats.sent_ok++; else s_stats.sent_failed++; } } } bool notifier_send(uint32_t delay_ms) { event_msg_t msg = { .delay_ms = delay_ms }; if (xQueueSend(s_q, &msg, 0) != pdTRUE) { s_stats.dropped++; ESP_LOGW(TAG, "event queue full, dropped"); return false; } return true; } void notifier_get_stats(notifier_stats_t *out) { *out = s_stats; out->wifi_configured = strlen(CONFIG_BUZZ_WIFI_SSID) > 0; out->endpoint_configured = strlen(CONFIG_BUZZ_EVENT_URL) > 0; } esp_err_t notifier_init(void) { s_q = xQueueCreate(4, sizeof(event_msg_t)); s_eg = xEventGroupCreate(); if (!s_q || !s_eg) return ESP_ERR_NO_MEM; s_stats.endpoint_configured = strlen(CONFIG_BUZZ_EVENT_URL) > 0; if (!s_stats.endpoint_configured) { ESP_LOGW(TAG, "no event endpoint configured; buzz events will be dropped"); } if (!strlen(CONFIG_BUZZ_EVENT_SECRET)) { ESP_LOGW(TAG, "event secret is empty; configure it before using the endpoint"); } s_stats.wifi_configured = strlen(CONFIG_BUZZ_WIFI_SSID) > 0; if (s_stats.wifi_configured) { ESP_ERROR_CHECK(esp_netif_init()); ESP_ERROR_CHECK(esp_event_loop_create_default()); esp_netif_create_default_wifi_sta(); const esp_timer_create_args_t targs = { .callback = reconnect_cb, .name = "wifi_retry" }; ESP_ERROR_CHECK(esp_timer_create(&targs, &s_reconnect_timer)); wifi_init_config_t icfg = WIFI_INIT_CONFIG_DEFAULT(); ESP_ERROR_CHECK(esp_wifi_init(&icfg)); ESP_ERROR_CHECK(esp_event_handler_register(WIFI_EVENT, ESP_EVENT_ANY_ID, wifi_event_handler, NULL)); ESP_ERROR_CHECK(esp_event_handler_register(IP_EVENT, IP_EVENT_STA_GOT_IP, wifi_event_handler, NULL)); wifi_config_t wc = { 0 }; strlcpy((char *)wc.sta.ssid, CONFIG_BUZZ_WIFI_SSID, sizeof(wc.sta.ssid)); strlcpy((char *)wc.sta.password, CONFIG_BUZZ_WIFI_PASSWORD, sizeof(wc.sta.password)); wc.sta.threshold.authmode = strlen(CONFIG_BUZZ_WIFI_PASSWORD) ? WIFI_AUTH_WPA2_PSK : WIFI_AUTH_OPEN; wc.sta.sae_pwe_h2e = WPA3_SAE_PWE_BOTH; ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA)); ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &wc)); ESP_ERROR_CHECK(esp_wifi_start()); } else { ESP_LOGW(TAG, "no Wi-Fi SSID configured; event requests cannot be sent"); } BaseType_t ok = xTaskCreatePinnedToCore(net_task, "net", NET_TASK_STACK, NULL, NET_TASK_PRIO, NULL, 0); return ok == pdPASS ? ESP_OK : ESP_ERR_NO_MEM; }