Files
buzz_detector/main/notifier.c
T

222 lines
7.7 KiB
C

// Wi-Fi station with exponential-backoff reconnect and queued event POSTs.
// HTTP requests run in their own task so networking cannot block audio.
#include <stdio.h>
#include <string.h>
#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;
}