Files
chargeflow/components/buzzer/src/buzzer.c
2025-11-20 07:45:00 +00:00

542 lines
16 KiB
C
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// buzzer.c
#include "buzzer_events.h"
#include "evse_events.h"
#include "auth_events.h"
#include "network_events.h"
#include "esp_event.h"
#include "esp_log.h"
#include "esp_err.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/queue.h"
#include "driver/gpio.h"
#include "driver/ledc.h"
#include <time.h>
#include <string.h>
// ===================== Configuração padrão (pode migrar para Kconfig) =====================
#ifndef CONFIG_BUZZER_GPIO
#define CONFIG_BUZZER_GPIO GPIO_NUM_27
#endif
#ifndef CONFIG_BUZZER_MODE_PASSIVE // 1 = PASSIVE (PWM), 0 = ACTIVE (on/off)
#define CONFIG_BUZZER_MODE_PASSIVE 1
#endif
#ifndef CONFIG_BUZZER_FREQ_HZ
#define CONFIG_BUZZER_FREQ_HZ 3500
#endif
#ifndef CONFIG_BUZZER_DUTY_PCT
#define CONFIG_BUZZER_DUTY_PCT 70
#endif
#ifndef CONFIG_BUZZER_QUEUE_LEN
#define CONFIG_BUZZER_QUEUE_LEN 8
#endif
#ifndef CONFIG_BUZZER_TASK_STACK
#define CONFIG_BUZZER_TASK_STACK 2048
#endif
#ifndef CONFIG_BUZZER_TASK_PRIO
#define CONFIG_BUZZER_TASK_PRIO (tskIDLE_PRIORITY + 1)
#endif
#ifndef CONFIG_BUZZER_MIN_GAP_MS // anti-spam (gap mínimo entre toques)
#define CONFIG_BUZZER_MIN_GAP_MS 70
#endif
#ifndef CONFIG_BUZZER_ENABLE_DEFAULT
#define CONFIG_BUZZER_ENABLE_DEFAULT 1
#endif
#ifndef CONFIG_BUZZER_QUIET_START_MIN // quiet hours start (minutos desde 00:00)
#define CONFIG_BUZZER_QUIET_START_MIN (22 * 60)
#endif
#ifndef CONFIG_BUZZER_QUIET_END_MIN // quiet hours end (minutos desde 00:00)
#define CONFIG_BUZZER_QUIET_END_MIN (7 * 60)
#endif
// ===================== Tipos e estado =====================
static const char *TAG = "Buzzer";
typedef enum
{
BUZZER_MODE_ACTIVE = 0,
BUZZER_MODE_PASSIVE
} buzzer_mode_t;
typedef struct
{
uint16_t on_ms;
uint16_t off_ms;
} buzzer_step_t;
typedef struct
{
const buzzer_step_t *steps;
size_t length;
} buzzer_pattern_t;
typedef struct
{
buzzer_mode_t mode;
gpio_num_t gpio;
// PASSIVE (PWM)
uint32_t freq_hz;
uint8_t duty_percent; // 0100
ledc_mode_t ledc_speed_mode; // LEDC_LOW_SPEED_MODE
ledc_timer_t ledc_timer; // LEDC_TIMER_1
ledc_channel_t ledc_channel; // LEDC_CHANNEL_1
ledc_timer_bit_t duty_resolution; // LEDC_TIMER_10_BIT
} buzzer_config_t;
// Controlo runtime
static struct
{
bool enabled;
uint16_t quiet_start_min; // [0..1440)
uint16_t quiet_end_min; // [0..1440)
bool quiet_enabled;
} s_buzzer_ctl = {
.enabled = CONFIG_BUZZER_ENABLE_DEFAULT,
.quiet_start_min = CONFIG_BUZZER_QUIET_START_MIN,
.quiet_end_min = CONFIG_BUZZER_QUIET_END_MIN,
.quiet_enabled = false,
};
// Hardware cfg
static buzzer_config_t s_buzzer_cfg = {
.mode = CONFIG_BUZZER_MODE_PASSIVE ? BUZZER_MODE_PASSIVE : BUZZER_MODE_ACTIVE,
.gpio = CONFIG_BUZZER_GPIO,
.freq_hz = CONFIG_BUZZER_FREQ_HZ,
.duty_percent = CONFIG_BUZZER_DUTY_PCT,
.ledc_speed_mode = LEDC_LOW_SPEED_MODE,
.ledc_timer = LEDC_TIMER_1,
.ledc_channel = LEDC_CHANNEL_1,
.duty_resolution = LEDC_TIMER_10_BIT};
// Fila e task
static QueueHandle_t s_buzzer_q = NULL;
static TaskHandle_t s_buzzer_task = NULL;
// Anti-spam básico
static TickType_t s_last_play_tick = 0;
static const TickType_t s_min_gap_ticks = pdMS_TO_TICKS(CONFIG_BUZZER_MIN_GAP_MS);
// ===================== Padrões (afinados) =====================
// NOTA: os enums BUZZER_PATTERN_* vêm de buzzer_events.h
// Curtos e claros
static const buzzer_step_t pattern_plugged[] = {{180, 120}}; // ~300 ms
static const buzzer_step_t pattern_card_read[] = {{80, 80}, {80, 0}}; // 160 ms audível
// Um pouco mais longos, ritmo marcante
static const buzzer_step_t pattern_unplugged[] = {{140, 140}, {140, 140}, {140, 0}}; // ~700 ms
static const buzzer_step_t pattern_card_add[] = {{100, 100}, {100, 100}, {120, 0}}; // ~520 ms
static const buzzer_step_t pattern_ap_start[] = {{250, 150}, {250, 0}}; // ~650 ms
// Charging mais curta (evento frequente)
static const buzzer_step_t pattern_charging[] = {{80, 90}, {90, 80}, {100, 0}}; // ~440 ms
// Denied mais “duro”
static const buzzer_step_t pattern_card_denied[] = {{250, 120}, {300, 0}}; // ~670 ms
// Fault crítico (duas trincas, ~1.7 s)
static const buzzer_step_t pattern_fault[] = {
{350, 120}, {350, 120}, {450, 250}, // bloco 1
{350, 120},
{350, 120},
{450, 0} // bloco 2
};
static const buzzer_pattern_t buzzer_patterns[] = {
[BUZZER_PATTERN_PLUGGED] = {pattern_plugged, sizeof(pattern_plugged) / sizeof(buzzer_step_t)},
[BUZZER_PATTERN_UNPLUGGED] = {pattern_unplugged, sizeof(pattern_unplugged) / sizeof(buzzer_step_t)},
[BUZZER_PATTERN_CHARGING] = {pattern_charging, sizeof(pattern_charging) / sizeof(buzzer_step_t)},
[BUZZER_PATTERN_AP_START] = {pattern_ap_start, sizeof(pattern_ap_start) / sizeof(buzzer_step_t)},
[BUZZER_PATTERN_CARD_READ] = {pattern_card_read, sizeof(pattern_card_read) / sizeof(buzzer_step_t)},
[BUZZER_PATTERN_CARD_ADD] = {pattern_card_add, sizeof(pattern_card_add) / sizeof(buzzer_step_t)},
[BUZZER_PATTERN_CARD_DENIED] = {pattern_card_denied, sizeof(pattern_card_denied) / sizeof(buzzer_step_t)},
[BUZZER_PATTERN_FAULT] = {pattern_fault, sizeof(pattern_fault) / sizeof(buzzer_step_t)},
};
// ===================== Helpers HW =====================
static inline uint32_t duty_from_percent(uint8_t pct, ledc_timer_bit_t res)
{
if (pct == 0)
return 0;
if (pct > 100)
pct = 100;
uint32_t max = (1U << res) - 1U;
return (uint32_t)((pct * max) / 100U);
}
static inline bool is_pattern_valid(buzzer_pattern_id_t id)
{
return (id > BUZZER_PATTERN_NONE && id < BUZZER_PATTERN_MAX);
}
static void buzzer_on(void)
{
if (s_buzzer_cfg.mode == BUZZER_MODE_PASSIVE)
{
// Não mudar freq a cada beep; já foi configurada na init.
esp_err_t err1 = ledc_set_duty(s_buzzer_cfg.ledc_speed_mode,
s_buzzer_cfg.ledc_channel,
duty_from_percent(s_buzzer_cfg.duty_percent, s_buzzer_cfg.duty_resolution));
if (err1 != ESP_OK)
ESP_LOGW(TAG, "ledc_set_duty err=%s", esp_err_to_name(err1));
esp_err_t err2 = ledc_update_duty(s_buzzer_cfg.ledc_speed_mode, s_buzzer_cfg.ledc_channel);
if (err2 != ESP_OK)
ESP_LOGW(TAG, "ledc_update_duty err=%s", esp_err_to_name(err2));
}
else
{
gpio_set_level(s_buzzer_cfg.gpio, 1);
}
}
static void buzzer_off(void)
{
if (s_buzzer_cfg.mode == BUZZER_MODE_PASSIVE)
{
esp_err_t err1 = ledc_set_duty(s_buzzer_cfg.ledc_speed_mode, s_buzzer_cfg.ledc_channel, 0);
if (err1 != ESP_OK)
ESP_LOGW(TAG, "ledc_set_duty(0) err=%s", esp_err_to_name(err1));
esp_err_t err2 = ledc_update_duty(s_buzzer_cfg.ledc_speed_mode, s_buzzer_cfg.ledc_channel);
if (err2 != ESP_OK)
ESP_LOGW(TAG, "ledc_update_duty err=%s", esp_err_to_name(err2));
}
else
{
gpio_set_level(s_buzzer_cfg.gpio, 0);
}
}
static bool in_quiet_hours(void)
{
if (!s_buzzer_ctl.quiet_enabled)
return false;
time_t now = time(NULL);
struct tm lt;
if (localtime_r(&now, &lt) == NULL)
return false;
uint16_t minutes = (uint16_t)(lt.tm_hour * 60 + lt.tm_min);
uint16_t start = s_buzzer_ctl.quiet_start_min;
uint16_t end = s_buzzer_ctl.quiet_end_min;
if (start == end)
return false; // desativado
if (start < end)
{
return (minutes >= start && minutes < end);
}
else
{ // janela cruza meia-noite
return (minutes >= start || minutes < end);
}
}
// ===================== Execução do padrão (apenas dentro da task) =====================
static void buzzer_execute(buzzer_pattern_id_t pattern_id)
{
if (!is_pattern_valid(pattern_id))
{
ESP_LOGW(TAG, "Invalid buzzer pattern id: %d", pattern_id);
return;
}
const buzzer_pattern_t *pattern = &buzzer_patterns[pattern_id];
for (size_t i = 0; i < pattern->length; i++)
{
buzzer_on();
vTaskDelay(pdMS_TO_TICKS(pattern->steps[i].on_ms));
buzzer_off();
if (pattern->steps[i].off_ms > 0)
{
vTaskDelay(pdMS_TO_TICKS(pattern->steps[i].off_ms));
}
}
}
// ===================== Task & Fila =====================
static void buzzer_task(void *arg)
{
buzzer_pattern_id_t id;
for (;;)
{
if (xQueueReceive(s_buzzer_q, &id, portMAX_DELAY) == pdTRUE)
{
ESP_LOGD(TAG, "dequeue pattern %d", id); // na buzzer_task()
if (!s_buzzer_ctl.enabled)
continue;
// Quiet hours: permitir apenas alertas críticos/negativos
if (in_quiet_hours())
{
if (!(id == BUZZER_PATTERN_FAULT || id == BUZZER_PATTERN_CARD_DENIED))
{
continue;
}
}
// Anti-spam global
TickType_t now = xTaskGetTickCount();
if ((now - s_last_play_tick) < s_min_gap_ticks)
{
continue;
}
s_last_play_tick = now;
buzzer_execute(id);
}
}
}
// ===================== API pública =====================
void buzzer_play(buzzer_pattern_id_t id)
{
ESP_LOGD(TAG, "enqueue pattern %d", id); // dentro de buzzer_play()
if (!is_pattern_valid(id) || s_buzzer_q == NULL)
return;
(void)xQueueSend(s_buzzer_q, &id, 0);
}
void buzzer_stop(void)
{
// Interrompe imediatamente: esvazia fila e desliga
if (s_buzzer_q)
xQueueReset(s_buzzer_q);
buzzer_off();
}
void buzzer_set_enabled(bool enabled)
{
s_buzzer_ctl.enabled = enabled;
if (!enabled)
buzzer_off();
}
bool buzzer_get_enabled(void)
{
return s_buzzer_ctl.enabled;
}
void buzzer_set_quiet_hours(bool enabled, uint16_t start_min, uint16_t end_min)
{
s_buzzer_ctl.quiet_enabled = enabled;
if (start_min < 1440)
s_buzzer_ctl.quiet_start_min = start_min;
if (end_min < 1440)
s_buzzer_ctl.quiet_end_min = end_min;
}
// Opcional: mudar tom dinamicamente (apenas se o timer for exclusivo do buzzer)
esp_err_t buzzer_set_frequency(uint32_t hz)
{
if (s_buzzer_cfg.mode != BUZZER_MODE_PASSIVE)
return ESP_ERR_INVALID_STATE;
if (hz < 50 || hz > 20000)
return ESP_ERR_INVALID_ARG;
s_buzzer_cfg.freq_hz = hz;
return ledc_set_freq(s_buzzer_cfg.ledc_speed_mode, s_buzzer_cfg.ledc_timer, hz);
}
void buzzer_set_duty_percent(uint8_t pct)
{
if (pct > 100)
pct = 100;
s_buzzer_cfg.duty_percent = pct;
}
// ===================== Event Handlers =====================
static void buzzer_event_handler(void *arg, esp_event_base_t base, int32_t id, void *data)
{
if (base != BUZZER_EVENTS || id != BUZZER_EVENT_PLAY_PATTERN || data == NULL)
return;
buzzer_event_data_t *evt = (buzzer_event_data_t *)data;
if (!is_pattern_valid(evt->pattern))
{
ESP_LOGW(TAG, "Invalid buzzer pattern received: %d", evt->pattern);
return;
}
buzzer_play(evt->pattern);
}
static void evse_event_handler(void *arg, esp_event_base_t base, int32_t id, void *data)
{
if (base != EVSE_EVENTS || id != EVSE_EVENT_STATE_CHANGED || data == NULL)
return;
const evse_state_event_data_t *evt = (const evse_state_event_data_t *)data;
ESP_LOGD(TAG, "EVSE event: state=%d", evt->state);
buzzer_event_data_t buzzer_evt = {0};
switch (evt->state)
{
case EVSE_STATE_EVENT_IDLE:
buzzer_evt.pattern = BUZZER_PATTERN_UNPLUGGED;
break;
case EVSE_STATE_EVENT_WAITING:
buzzer_evt.pattern = BUZZER_PATTERN_PLUGGED;
break;
case EVSE_STATE_EVENT_CHARGING:
buzzer_evt.pattern = BUZZER_PATTERN_CHARGING;
break;
case EVSE_STATE_EVENT_FAULT:
// Preempta qualquer beep em andamento
buzzer_stop();
buzzer_evt.pattern = BUZZER_PATTERN_FAULT;
break;
default:
return;
}
esp_event_post(BUZZER_EVENTS, BUZZER_EVENT_PLAY_PATTERN, &buzzer_evt, sizeof(buzzer_evt), portMAX_DELAY);
}
static void network_event_handler(void *handler_args, esp_event_base_t base, int32_t id, void *event_data)
{
if (base != NETWORK_EVENTS)
return;
if (id == NETWORK_EVENT_AP_STARTED)
{
buzzer_event_data_t evt = {.pattern = BUZZER_PATTERN_AP_START};
esp_event_post(BUZZER_EVENTS, BUZZER_EVENT_PLAY_PATTERN, &evt, sizeof(evt), portMAX_DELAY);
}
}
static void auth_event_handler(void *arg, esp_event_base_t base, int32_t id, void *event_data)
{
if (base != AUTH_EVENTS || event_data == NULL)
return;
buzzer_event_data_t buzzer_evt = {0};
if (id == AUTH_EVENT_TAG_PROCESSED)
{
const auth_tag_event_data_t *evt = (const auth_tag_event_data_t *)event_data;
ESP_LOGD(TAG, "AUTH processed: tag=%s authorized=%d", evt->tag, evt->authorized);
buzzer_evt.pattern = evt->authorized ? BUZZER_PATTERN_CARD_READ : BUZZER_PATTERN_CARD_DENIED;
}
else if (id == AUTH_EVENT_TAG_SAVED)
{
buzzer_evt.pattern = BUZZER_PATTERN_CARD_ADD;
}
else
{
return;
}
esp_event_post(BUZZER_EVENTS, BUZZER_EVENT_PLAY_PATTERN, &buzzer_evt, sizeof(buzzer_evt), portMAX_DELAY);
}
// ===================== Inicialização / Deinit =====================
void buzzer_init(void)
{
// Config HW
if (s_buzzer_cfg.mode == BUZZER_MODE_PASSIVE)
{
ledc_timer_config_t tcfg = {
.speed_mode = s_buzzer_cfg.ledc_speed_mode,
.duty_resolution = s_buzzer_cfg.duty_resolution,
.timer_num = s_buzzer_cfg.ledc_timer,
.freq_hz = s_buzzer_cfg.freq_hz,
.clk_cfg = LEDC_AUTO_CLK};
ESP_ERROR_CHECK(ledc_timer_config(&tcfg));
ledc_channel_config_t ccfg = {
.gpio_num = s_buzzer_cfg.gpio,
.speed_mode = s_buzzer_cfg.ledc_speed_mode,
.channel = s_buzzer_cfg.ledc_channel,
.intr_type = LEDC_INTR_DISABLE,
.timer_sel = s_buzzer_cfg.ledc_timer,
.duty = 0,
.hpoint = 0};
ESP_ERROR_CHECK(ledc_channel_config(&ccfg));
}
else
{
gpio_config_t io = {
.pin_bit_mask = (1ULL << s_buzzer_cfg.gpio),
.mode = GPIO_MODE_OUTPUT,
.pull_down_en = 0,
.pull_up_en = 0,
.intr_type = GPIO_INTR_DISABLE};
ESP_ERROR_CHECK(gpio_config(&io));
gpio_set_level(s_buzzer_cfg.gpio, 0);
}
buzzer_set_quiet_hours(false, 0, 0);
buzzer_set_enabled(true);
buzzer_off();
// Fila + Task
s_buzzer_q = xQueueCreate(CONFIG_BUZZER_QUEUE_LEN, sizeof(buzzer_pattern_id_t));
configASSERT(s_buzzer_q != NULL);
BaseType_t ok = xTaskCreatePinnedToCore(
buzzer_task,
"buzzer_task",
CONFIG_BUZZER_TASK_STACK,
NULL,
CONFIG_BUZZER_TASK_PRIO,
&s_buzzer_task,
tskNO_AFFINITY);
configASSERT(ok == pdPASS);
// Handlers
ESP_ERROR_CHECK(esp_event_handler_register(BUZZER_EVENTS, BUZZER_EVENT_PLAY_PATTERN, buzzer_event_handler, NULL));
ESP_ERROR_CHECK(esp_event_handler_register(EVSE_EVENTS, EVSE_EVENT_STATE_CHANGED, evse_event_handler, NULL));
ESP_ERROR_CHECK(esp_event_handler_register(AUTH_EVENTS, AUTH_EVENT_TAG_PROCESSED, auth_event_handler, NULL));
ESP_ERROR_CHECK(esp_event_handler_register(AUTH_EVENTS, AUTH_EVENT_TAG_SAVED, auth_event_handler, NULL));
ESP_ERROR_CHECK(esp_event_handler_register(NETWORK_EVENTS, NETWORK_EVENT_AP_STARTED, network_event_handler, NULL));
ESP_LOGI(TAG, "Buzzer initialized on GPIO %d (%s), freq=%lu Hz, duty=%u%%, enabled=%d",
s_buzzer_cfg.gpio,
s_buzzer_cfg.mode == BUZZER_MODE_PASSIVE ? "passive/PWM" : "active/ON-OFF",
(unsigned long)s_buzzer_cfg.freq_hz,
(unsigned)s_buzzer_cfg.duty_percent,
(int)s_buzzer_ctl.enabled);
}
void buzzer_deinit(void)
{
(void)esp_event_handler_unregister(BUZZER_EVENTS, BUZZER_EVENT_PLAY_PATTERN, buzzer_event_handler);
(void)esp_event_handler_unregister(EVSE_EVENTS, EVSE_EVENT_STATE_CHANGED, evse_event_handler);
(void)esp_event_handler_unregister(AUTH_EVENTS, AUTH_EVENT_TAG_PROCESSED, auth_event_handler);
(void)esp_event_handler_unregister(AUTH_EVENTS, AUTH_EVENT_TAG_SAVED, auth_event_handler);
(void)esp_event_handler_unregister(NETWORK_EVENTS, NETWORK_EVENT_AP_STARTED, network_event_handler);
if (s_buzzer_q)
{
vQueueDelete(s_buzzer_q);
s_buzzer_q = NULL;
}
if (s_buzzer_task)
{
vTaskDelete(s_buzzer_task);
s_buzzer_task = NULL;
}
buzzer_off();
ESP_LOGI(TAG, "Buzzer deinitialized");
}