feature: alarm

This commit is contained in:
Mikkeli Matlock
2026-02-15 21:11:33 +09:00
parent 12dbbd8942
commit dca989a01b
14 changed files with 543 additions and 1 deletions

View File

@@ -0,0 +1,5 @@
idf_component_register(
SRCS "audio_client.cpp"
REQUIRES espressif__esp_websocket_client port_bsp
PRIV_REQUIRES esp_event json
INCLUDE_DIRS "./")

View File

@@ -0,0 +1,224 @@
#include "audio_client.h"
#include "codec_bsp.h"
#include "esp_websocket_client.h"
#include "esp_log.h"
#include "cJSON.h"
#include <string.h>
#include <freertos/FreeRTOS.h>
#include <freertos/queue.h>
#include <freertos/task.h>
#include <esp_heap_caps.h>
static const char *TAG = "audio_client";
#define AUDIO_CHUNK_SIZE 4096
#define PCM_QUEUE_DEPTH 10
#define PLAYBACK_STACK_SIZE (4 * 1024)
#define PLAYBACK_PRIORITY 4
#define WS_BUFFER_SIZE 8192
static esp_websocket_client_handle_t s_client = NULL;
static CodecPort *s_codec = NULL;
static QueueHandle_t s_pcm_queue = NULL;
static TaskHandle_t s_playback_task = NULL;
static volatile audio_state_t s_state = AUDIO_IDLE;
static volatile bool s_playing = false;
/* Forward declarations */
static void playback_task(void *arg);
static void ws_event_handler(void *arg, esp_event_base_t event_base,
int32_t event_id, void *event_data);
/* ---------- Queue helpers ---------- */
static void flush_queue(void)
{
uint8_t *chunk;
while (xQueueReceive(s_pcm_queue, &chunk, 0) == pdTRUE) {
heap_caps_free(chunk);
}
}
/* ---------- WebSocket event handler ---------- */
static void handle_text_frame(const char *data, int len)
{
cJSON *root = cJSON_ParseWithLength(data, len);
if (!root) {
ESP_LOGW(TAG, "JSON parse failed");
return;
}
cJSON *type = cJSON_GetObjectItem(root, "type");
if (!cJSON_IsString(type)) {
cJSON_Delete(root);
return;
}
if (strcmp(type->valuestring, "alarm_start") == 0) {
int sr = 24000;
int ch = 2;
int bits = 16;
cJSON *item;
item = cJSON_GetObjectItem(root, "sample_rate");
if (cJSON_IsNumber(item)) sr = item->valueint;
item = cJSON_GetObjectItem(root, "channels");
if (cJSON_IsNumber(item)) ch = item->valueint;
item = cJSON_GetObjectItem(root, "bits");
if (cJSON_IsNumber(item)) bits = item->valueint;
ESP_LOGI(TAG, "Alarm start: %dHz %dch %dbit", sr, ch, bits);
/* Flush any stale data */
flush_queue();
/* Open codec for playback */
s_codec->CodecPort_SetInfo("es8311", 1, sr, ch, bits);
s_codec->CodecPort_SetSpeakerVol(70);
s_playing = true;
s_state = AUDIO_PLAYING;
} else if (strcmp(type->valuestring, "alarm_stop") == 0) {
ESP_LOGI(TAG, "Alarm stop");
s_playing = false;
/* Let playback task drain remaining chunks, then close */
vTaskDelay(pdMS_TO_TICKS(100));
flush_queue();
s_codec->CodecPort_CloseSpeaker();
s_state = AUDIO_CONNECTED;
}
cJSON_Delete(root);
}
static void handle_binary_frame(const uint8_t *data, int len)
{
if (!s_playing) return;
uint8_t *chunk = heap_caps_malloc(len, MALLOC_CAP_SPIRAM);
if (!chunk) {
ESP_LOGW(TAG, "PSRAM alloc failed (%d bytes)", len);
return;
}
memcpy(chunk, data, len);
if (xQueueSend(s_pcm_queue, &chunk, 0) != pdTRUE) {
ESP_LOGW(TAG, "PCM queue full, dropping chunk");
heap_caps_free(chunk);
}
}
static void ws_event_handler(void *arg, esp_event_base_t event_base,
int32_t event_id, void *event_data)
{
esp_websocket_event_data_t *ev = (esp_websocket_event_data_t *)event_data;
switch (event_id) {
case WEBSOCKET_EVENT_CONNECTED:
ESP_LOGI(TAG, "Audio WS connected");
s_state = AUDIO_CONNECTED;
break;
case WEBSOCKET_EVENT_DISCONNECTED:
ESP_LOGW(TAG, "Audio WS disconnected");
s_playing = false;
flush_queue();
s_state = AUDIO_IDLE;
break;
case WEBSOCKET_EVENT_DATA:
if (ev->op_code == 0x01 && ev->data_len > 0) {
handle_text_frame(ev->data_ptr, ev->data_len);
} else if (ev->op_code == 0x02 && ev->data_len > 0) {
handle_binary_frame((const uint8_t *)ev->data_ptr, ev->data_len);
}
break;
case WEBSOCKET_EVENT_ERROR:
ESP_LOGE(TAG, "Audio WS error");
s_playing = false;
s_state = AUDIO_ERROR;
break;
default:
break;
}
}
/* ---------- Playback task ---------- */
static void playback_task(void *arg)
{
uint8_t *chunk;
for (;;) {
if (xQueueReceive(s_pcm_queue, &chunk, pdMS_TO_TICKS(500)) == pdTRUE) {
if (s_playing) {
s_codec->CodecPort_PlayWrite(chunk, AUDIO_CHUNK_SIZE);
}
heap_caps_free(chunk);
}
}
}
/* ---------- Public API ---------- */
void audio_client_init(const char *uri, void *codec)
{
s_codec = (CodecPort *)codec;
s_pcm_queue = xQueueCreate(PCM_QUEUE_DEPTH, sizeof(uint8_t *));
if (!s_pcm_queue) {
ESP_LOGE(TAG, "Failed to create PCM queue");
return;
}
esp_websocket_client_config_t config = {};
config.uri = uri;
config.reconnect_timeout_ms = 5000;
config.buffer_size = WS_BUFFER_SIZE;
s_client = esp_websocket_client_init(&config);
esp_websocket_register_events(s_client, WEBSOCKET_EVENT_ANY, ws_event_handler, NULL);
ESP_LOGI(TAG, "Audio client initialized: %s", uri);
}
void audio_client_start(void)
{
if (!s_client) return;
/* Create playback task pinned to Core 1 */
xTaskCreatePinnedToCore(playback_task, "audio_play", PLAYBACK_STACK_SIZE,
NULL, PLAYBACK_PRIORITY, &s_playback_task, 1);
esp_websocket_client_start(s_client);
ESP_LOGI(TAG, "Audio client started");
}
void audio_client_stop(void)
{
if (!s_client) return;
s_playing = false;
esp_websocket_client_stop(s_client);
flush_queue();
if (s_playback_task) {
vTaskDelete(s_playback_task);
s_playback_task = NULL;
}
s_state = AUDIO_IDLE;
ESP_LOGI(TAG, "Audio client stopped");
}
audio_state_t audio_client_get_state(void)
{
return s_state;
}

View File

@@ -0,0 +1,34 @@
#pragma once
#include <stdbool.h>
#ifdef __cplusplus
extern "C" {
#endif
typedef enum {
AUDIO_IDLE = 0,
AUDIO_CONNECTED,
AUDIO_PLAYING,
AUDIO_ERROR,
} audio_state_t;
/**
* Initialize the audio streaming client.
* @param uri WebSocket URI (e.g. "ws://192.168.2.199:8766")
* @param codec Pointer to CodecPort instance (passed as void* for C linkage)
*/
void audio_client_init(const char *uri, void *codec);
/** Start the WebSocket connection and playback task. */
void audio_client_start(void);
/** Stop playback and disconnect. */
void audio_client_stop(void);
/** Get current audio client state. */
audio_state_t audio_client_get_state(void);
#ifdef __cplusplus
}
#endif

View File

@@ -4,5 +4,6 @@
#define WIFI_SSID "Novoyuuparosk_H3C"
#define WIFI_PASSWORD "northwich"
#define WS_SERVER_URI "ws://192.168.2.199:8765"
#define AUDIO_SERVER_URI "ws://192.168.2.199:8766"
#endif

View File

@@ -1,5 +1,5 @@
idf_component_register(
SRCS "user_app.cpp" "alert.cpp"
REQUIRES app_bsp
PRIV_REQUIRES esp_wifi_bsp ws_client dashboard_ui port_bsp esp_timer codec_board
PRIV_REQUIRES esp_wifi_bsp ws_client dashboard_ui port_bsp esp_timer codec_board audio_client
INCLUDE_DIRS "./")

View File

@@ -1,4 +1,5 @@
#include "alert.h"
#include "audio_client.h"
#include "codec_bsp.h"
#include "i2c_bsp.h"
#include <esp_log.h>
@@ -84,6 +85,7 @@ void alert_trigger(alert_type_t type)
if (type >= ALERT_TYPE_COUNT) return;
if (s_muted) return;
if (!s_tone_buf || !s_codec) return;
if (audio_client_get_state() == AUDIO_PLAYING) return; /* don't fight over codec */
if (xSemaphoreTake(s_alert_mutex, pdMS_TO_TICKS(50)) != pdTRUE) return;

View File

@@ -9,6 +9,7 @@
#include "button_bsp.h"
#include "codec_bsp.h"
#include "alert.h"
#include "audio_client.h"
#include "lvgl_bsp.h"
#include <esp_log.h>
@@ -57,6 +58,9 @@ void UserApp_AppInit(void)
alert_init();
alert_set_codec(s_codec);
/* Audio streaming client */
audio_client_init(AUDIO_SERVER_URI, s_codec);
/* WebSocket client init (not started yet) */
ws_client_init(WS_SERVER_URI);
ws_client_set_data_callback(ws_data_cb);
@@ -78,6 +82,9 @@ void UserApp_TaskInit(void)
/* Start WebSocket client */
ws_client_start();
/* Start audio streaming client */
audio_client_start();
/* Sensor polling task - Core 1, 4KB stack */
xTaskCreatePinnedToCore(sensor_task, "sensor", 4 * 1024, NULL, 3, NULL, 1);