#include "user_app.h" #include "dashboard_ui.h" #include "ws_client.h" #include "esp_wifi_bsp.h" #include "wifi_config.h" #include "i2c_bsp.h" #include "i2c_equipment.h" #include "adc_bsp.h" #include "button_bsp.h" #include "codec_bsp.h" #include "alert.h" #include "audio_client.h" #include "lvgl_bsp.h" #include #include #include #include #include static const char *TAG = "user_app"; /* Hardware objects */ static I2cMasterBus *s_i2c = nullptr; static Shtc3Port *s_shtc3 = nullptr; static CodecPort *s_codec = nullptr; /* Forward declarations */ static void sensor_task(void *arg); static void button_task(void *arg); static void ws_data_cb(const pi_stats_t *stats); static void ws_state_cb(ws_state_t state); void UserApp_AppInit(void) { ESP_LOGI(TAG, "Initializing hardware..."); /* I2C bus (SCL=14, SDA=13, port 0) */ s_i2c = new I2cMasterBus(14, 13, 0); /* SHTC3 temperature/humidity sensor */ s_shtc3 = new Shtc3Port(*s_i2c); ESP_LOGI(TAG, "SHTC3 ID: 0x%04X", s_shtc3->Shtc3_GetShtc3Id()); /* RTC */ Rtc_Setup(s_i2c, 0x51); /* ADC for battery */ Adc_PortInit(); /* Buttons */ Custom_ButtonInit(); /* Codec */ s_codec = new CodecPort(*s_i2c, "S3_RLCD_4_2"); /* Alert system */ 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); ws_client_set_state_callback(ws_state_cb); ESP_LOGI(TAG, "Hardware init complete"); } void UserApp_UiInit(void) { dashboard_ui_create(); ESP_LOGI(TAG, "Dashboard UI created"); } void UserApp_TaskInit(void) { ESP_LOGI(TAG, "Starting background tasks..."); /* Start WebSocket client */ ws_client_start(); /* Sensor polling task - Core 1, 4KB stack */ TaskHandle_t sensor_handle = NULL; xTaskCreatePinnedToCore(sensor_task, "sensor", 4 * 1024, NULL, 3, &sensor_handle, 1); /* Start audio streaming client, notify sensor task on new images */ audio_client_set_image_notify_task(sensor_handle); audio_client_start(); /* Button handling task - Core 1 */ xTaskCreatePinnedToCore(button_task, "button", 2 * 1024, NULL, 2, NULL, 1); ESP_LOGI(TAG, "All tasks started"); } /* ---------- WebSocket callbacks ---------- */ static void rtc_sync_if_needed(const pi_stats_t *stats) { if (!stats->time_valid) return; rtcTimeStruct_t rtc = {}; Rtc_GetTime(&rtc); /* Convert both to seconds-since-midnight for comparison */ int pi_secs = stats->time_hour * 3600 + stats->time_minute * 60 + stats->time_second; int rtc_secs = rtc.hour * 3600 + rtc.minute * 60 + rtc.second; int delta = pi_secs - rtc_secs; if (delta < 0) delta = -delta; /* Also check date mismatch as an immediate trigger */ bool date_mismatch = (rtc.year != stats->time_year || rtc.month != stats->time_month || rtc.day != stats->time_day); if (date_mismatch || delta > 60) { Rtc_SetTime(stats->time_year, stats->time_month, stats->time_day, stats->time_hour, stats->time_minute, stats->time_second); ESP_LOGI(TAG, "RTC synced from Pi: %04d-%02d-%02d %02d:%02d:%02d (drift: %ds)", stats->time_year, stats->time_month, stats->time_day, stats->time_hour, stats->time_minute, stats->time_second, delta); } } static void ws_data_cb(const pi_stats_t *stats) { /* Check alert conditions */ if (stats->cpu_temp > 80.0f) { alert_trigger(ALERT_HIGH_TEMP); } for (int i = 0; i < stats->service_count; i++) { if (stats->services[i].status != SVC_RUNNING) { alert_trigger(ALERT_SERVICE_DOWN); break; } } /* Sync RTC if Pi time drifts from board clock */ rtc_sync_if_needed(stats); /* Update UI under LVGL lock */ if (Lvgl_lock(100)) { dashboard_ui_update_stats(stats); char ip_buf[20]; wifi_sta_get_ip_str(ip_buf, sizeof(ip_buf)); dashboard_ui_update_connection( ws_client_get_state(), wifi_sta_is_connected(), ip_buf ); Lvgl_unlock(); } } static void ws_state_cb(ws_state_t state) { ESP_LOGI(TAG, "WS state changed: %d", state); if (state == WS_STATE_DISCONNECTED || state == WS_STATE_ERROR) { alert_trigger(ALERT_WS_DISCONNECT); } else if (state == WS_STATE_CONNECTED) { alert_trigger(ALERT_CONNECT_OK); } /* Update connection indicator */ if (Lvgl_lock(100)) { char ip_buf[20]; wifi_sta_get_ip_str(ip_buf, sizeof(ip_buf)); dashboard_ui_update_connection(state, wifi_sta_is_connected(), ip_buf); Lvgl_unlock(); } } /* ---------- Sensor task ---------- */ static void sensor_task(void *arg) { float temp = 0, humidity = 0; rtcTimeStruct_t rtc_time = {}; int sensor_divider = 0; TickType_t last_sensor_tick = xTaskGetTickCount(); for (;;) { /* * Sleep until either: * - a new status image arrives (xTaskNotifyGive from audio_client), or * - 1 second elapses (clock refresh cadence) */ ulTaskNotifyTake(pdTRUE, pdMS_TO_TICKS(1000)); /* Check for status image updates immediately */ bool img_updated = false; const lv_img_dsc_t *img = audio_client_get_status_image(&img_updated); if (img_updated && Lvgl_lock(100)) { dashboard_ui_update_status_image(img); Lvgl_unlock(); } /* Sensor + clock updates at ~1s cadence (skip if woken early) */ TickType_t now = xTaskGetTickCount(); if ((now - last_sensor_tick) < pdMS_TO_TICKS(900)) { continue; } last_sensor_tick = now; /* Read RTC */ Rtc_GetTime(&rtc_time); /* Read SHTC3 + battery every 5 seconds */ if (sensor_divider == 0) { s_shtc3->Shtc3_Wakeup(); vTaskDelay(pdMS_TO_TICKS(20)); s_shtc3->Shtc3_ReadTempHumi(&temp, &humidity); s_shtc3->Shtc3_Sleep(); float batt_v = Adc_GetBatteryVoltage(); uint8_t batt = Adc_GetBatteryLevel(); bool charging = Adc_IsCharging(); if (Lvgl_lock(100)) { dashboard_ui_update_local(temp, humidity, batt, charging, batt_v); Lvgl_unlock(); } } sensor_divider = (sensor_divider + 1) % 5; /* Update clock */ if (Lvgl_lock(100)) { dashboard_ui_update_time(rtc_time.hour, rtc_time.minute, rtc_time.second, rtc_time.year, rtc_time.month, rtc_time.day, rtc_time.week); Lvgl_unlock(); } } } /* ---------- Button task ---------- */ static void button_task(void *arg) { for (;;) { /* Wait for GP18 button event (single click = bit 0) */ EventBits_t bits = xEventGroupWaitBits( GP18ButtonGroups, set_bit_button(0), /* single press bit */ pdTRUE, /* clear on exit */ pdFALSE, /* any bit */ pdMS_TO_TICKS(500) ); if (bits & set_bit_button(0)) { bool muted = !alert_is_muted(); alert_mute(muted); ESP_LOGI(TAG, "GP18 pressed: alerts %s", muted ? "muted" : "unmuted"); } } }