// Wi-Fi station with exponential-backoff reconnect + ntfy.sh HTTPS notifications. // Sending happens in its own task fed by a queue so networking can never 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 "config_store.h" #include "notifier.h" static const char *TAG = "notify"; #define WIFI_CONNECTED_BIT BIT0 #define SEND_ATTEMPTS 4 #define NET_TASK_STACK 8192 #define NET_TASK_PRIO 5 #define BACKOFF_MIN_MS 1000 #define BACKOFF_MAX_MS 60000 typedef struct { notif_kind_t kind; char msg[96]; } notif_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 bool s_wifi_configured; 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); // ok if not running 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 esp_err_t post_ntfy(const char *topic, const char *title, const char *prio, const char *tags, const char *body) { char url[192]; size_t sl = strlen(CONFIG_BUZZ_NTFY_SERVER); while (sl > 0 && CONFIG_BUZZ_NTFY_SERVER[sl - 1] == '/') sl--; snprintf(url, sizeof(url), "%.*s/%s", (int)sl, CONFIG_BUZZ_NTFY_SERVER, topic); esp_http_client_config_t cfg = { .url = url, .method = HTTP_METHOD_POST, .timeout_ms = 10000, .crt_bundle_attach = esp_crt_bundle_attach, }; esp_http_client_handle_t c = esp_http_client_init(&cfg); if (!c) return ESP_ERR_NO_MEM; esp_http_client_set_header(c, "Title", title); esp_http_client_set_header(c, "Priority", prio); esp_http_client_set_header(c, "Tags", tags); esp_http_client_set_post_field(c, body, (int)strlen(body)); esp_err_t err = esp_http_client_perform(c); if (err == ESP_OK) { int st = esp_http_client_get_status_code(c); if (st != 200) { ESP_LOGW(TAG, "ntfy HTTP status %d", st); err = ESP_FAIL; } } else { ESP_LOGW(TAG, "HTTP perform: %s", esp_err_to_name(err)); } esp_http_client_cleanup(c); return err; } // Retry a few times with backoff while Wi-Fi is down / the request fails, then drop. static bool deliver(const char *topic, const char *title, const char *prio, const char *tags, const char *body) { uint32_t wait_ms = 2000; for (int attempt = 1; attempt <= SEND_ATTEMPTS; attempt++) { EventBits_t bits = xEventGroupWaitBits(s_eg, WIFI_CONNECTED_BIT, pdFALSE, pdTRUE, pdMS_TO_TICKS(15000)); if (bits & WIFI_CONNECTED_BIT) { if (post_ntfy(topic, title, prio, tags, body) == ESP_OK) { ESP_LOGI(TAG, "sent '%s' to %s (attempt %d)", body, topic, attempt); return true; } } else { ESP_LOGW(TAG, "Wi-Fi down (attempt %d/%d)", attempt, SEND_ATTEMPTS); } vTaskDelay(pdMS_TO_TICKS(wait_ms)); wait_ms *= 2; } ESP_LOGE(TAG, "giving up on '%s', dropped", body); return false; } static void net_task(void *arg) { int64_t last_hb_us = esp_timer_get_time(); notif_msg_t m; for (;;) { if (xQueueReceive(s_q, &m, pdMS_TO_TICKS(30000)) == pdTRUE) { bool ok = deliver(CONFIG_BUZZ_NTFY_TOPIC, "Door buzzer", m.kind == NOTIF_ALERT ? "high" : "default", m.kind == NOTIF_ALERT ? "bell" : "wrench", m.msg); if (ok) s_stats.sent_ok++; else s_stats.sent_failed++; last_hb_us = esp_timer_get_time(); // an alert proves liveness; restart the heartbeat timer } app_config_t cfg; config_get(&cfg); if (cfg.heartbeat_h > 0 && (esp_timer_get_time() - last_hb_us) >= (int64_t)cfg.heartbeat_h * 3600LL * 1000000LL) { last_hb_us = esp_timer_get_time(); if (deliver(CONFIG_BUZZ_NTFY_HB_TOPIC, "Door buzzer heartbeat", "min", "green_heart", "alive")) { s_stats.sent_ok++; } else { s_stats.sent_failed++; } } } } bool notifier_send(notif_kind_t kind, const char *msg) { notif_msg_t m = { .kind = kind }; snprintf(m.msg, sizeof(m.msg), "%s", msg); if (xQueueSend(s_q, &m, 0) != pdTRUE) { s_stats.dropped++; ESP_LOGW(TAG, "notification queue full, dropped"); return false; } return true; } void notifier_get_stats(notifier_stats_t *out) { *out = s_stats; out->wifi_configured = s_wifi_configured; } esp_err_t notifier_init(void) { s_q = xQueueCreate(4, sizeof(notif_msg_t)); s_eg = xEventGroupCreate(); if (!s_q || !s_eg) return ESP_ERR_NO_MEM; s_wifi_configured = strlen(CONFIG_BUZZ_WIFI_SSID) > 0; if (s_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 (menuconfig): notifications will be dropped"); } 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; }