#include #include #include #include "freertos/FreeRTOS.h" #include "freertos/task.h" #include "freertos/semphr.h" #include "esp_log.h" #include "esp_err.h" #include "esp_timer.h" #include "nvs_flash.h" #include "esp_event.h" #include "esp_netif.h" #include "esp_netif_ip_addr.h" #include "esp_wifi.h" #include "esp_http_client.h" // TLS bundle not required as a build dependency; keep header optional #ifdef __has_include # if __has_include("esp_crt_bundle.h") # include "esp_crt_bundle.h" # define HAS_CRT_BUNDLE 1 # endif #endif #include "driver/gpio.h" #include "driver/ledc.h" #include "driver/spi_master.h" #include "esp_lcd_panel_io.h" #include "esp_lcd_panel_vendor.h" #include "esp_lcd_panel_ops.h" #include "esp_lcd_types.h" #include "esp_heap_caps.h" #include #include "cJSON.h" // Kconfig symbols #include "sdkconfig.h" // Pins from Kconfig (with sensible fallbacks) #ifndef CONFIG_LCD_PIN_MOSI #define CONFIG_LCD_PIN_MOSI 7 #endif #ifndef CONFIG_LCD_PIN_SCLK #define CONFIG_LCD_PIN_SCLK 6 #endif #ifndef CONFIG_LCD_PIN_CS #define CONFIG_LCD_PIN_CS 5 #endif #ifndef CONFIG_LCD_PIN_DC #define CONFIG_LCD_PIN_DC 2 #endif #ifndef CONFIG_LCD_PIN_RST #define CONFIG_LCD_PIN_RST 4 #endif #ifndef CONFIG_LCD_PIN_BL #define CONFIG_LCD_PIN_BL 15 #endif #ifndef CONFIG_LCD_PIN_BUTTON #define CONFIG_LCD_PIN_BUTTON 0 #endif #ifndef CONFIG_LCD_PIN_LED #define CONFIG_LCD_PIN_LED 8 #endif #ifndef CONFIG_LCD_PIN_SPEAKER #define CONFIG_LCD_PIN_SPEAKER 25 #endif #ifndef CONFIG_LCD_PIN_BAT_EN #define CONFIG_LCD_PIN_BAT_EN 15 #endif // Display constants #define LCD_H_RES 240 #define LCD_V_RES 280 static const char *TAG = "GEMINI_GADGET"; typedef enum { UI_BOOT, UI_CONNECTING, UI_READY, UI_LISTENING, UI_THINKING, UI_SPEAKING, UI_ERROR } ui_state_t; // LCD handles static esp_lcd_panel_handle_t s_panel = NULL; static int s_rotation = 0; static int s_offset_y = 0; static int s_spi_clk_hz = 0; // Button semaphore to trigger a chat cycle static SemaphoreHandle_t s_btn_sem = NULL; // Tone control static void tone_init(void); static void tone_start(uint32_t freq_hz); static void tone_stop(void); static void tone_beep(uint32_t freq_hz, uint32_t duration_ms); // Wi-Fi static esp_err_t wifi_init_and_connect(const char *ssid, const char *pass, int timeout_ms); // HTTP / Gemini static char *gemini_generate_text(const char *api_key, const char *prompt, int *http_status_out); // UI helpers static void ui_set_state(ui_state_t state); static bool lcd_autoprobe(void); // Simple color helper (RGB565) static inline uint16_t rgb565(uint8_t r, uint8_t g, uint8_t b) { return ((r & 0xF8) << 8) | ((g & 0xFC) << 3) | (b >> 3); } static void draw_solid_color(uint16_t color) { if (!s_panel) return; static uint16_t *line = NULL; const int width = LCD_H_RES; const int height = LCD_V_RES; if (!line) { line = (uint16_t *)heap_caps_malloc(width * sizeof(uint16_t), MALLOC_CAP_DMA); } if (!line) { ESP_LOGE(TAG, "Failed to alloc line buffer"); return; } for (int x = 0; x < width; ++x) line[x] = color; for (int y = 0; y < height; ++y) { esp_lcd_panel_draw_bitmap(s_panel, 0, y, width, y + 1, line); } } static void color_sweep(void) { if (!s_panel) return; const uint16_t colors[] = { rgb565(255, 0, 0), rgb565(0, 255, 0), rgb565(0, 0, 255), rgb565(255, 255, 255), rgb565(0, 0, 0) }; for (size_t i = 0; i < sizeof(colors)/sizeof(colors[0]); ++i) { draw_solid_color(colors[i]); vTaskDelay(pdMS_TO_TICKS(200)); } } static void ui_set_state(ui_state_t state) { switch (state) { case UI_BOOT: ESP_LOGI(TAG, "STATE: BOOT"); draw_solid_color(rgb565(32, 32, 64)); break; case UI_CONNECTING: ESP_LOGI(TAG, "STATE: CONNECTING"); draw_solid_color(rgb565(64, 64, 0)); break; case UI_READY: ESP_LOGI(TAG, "STATE: READY"); draw_solid_color(rgb565(0, 64, 0)); break; case UI_LISTENING: ESP_LOGI(TAG, "STATE: LISTENING"); draw_solid_color(rgb565(0, 0, 128)); break; case UI_THINKING: ESP_LOGI(TAG, "STATE: THINKING"); draw_solid_color(rgb565(128, 0, 128)); break; case UI_SPEAKING: ESP_LOGI(TAG, "STATE: SPEAKING"); draw_solid_color(rgb565(0, 128, 128)); break; case UI_ERROR: default: ESP_LOGE(TAG, "STATE: ERROR"); draw_solid_color(rgb565(128, 0, 0)); break; } } // Button ISR static volatile uint64_t s_last_btn_us = 0; static void IRAM_ATTR button_isr(void *arg) { uint64_t now = esp_timer_get_time(); if ((now - s_last_btn_us) > 200000) { // 200ms debounce BaseType_t hpw = pdFALSE; xSemaphoreGiveFromISR(s_btn_sem, &hpw); if (hpw) portYIELD_FROM_ISR(); s_last_btn_us = now; } } static void gpio_init_all(void) { // Battery enable gpio_config_t io = { .pin_bit_mask = 1ULL << CONFIG_LCD_PIN_BAT_EN, .mode = GPIO_MODE_OUTPUT, .pull_up_en = GPIO_PULLUP_DISABLE, .pull_down_en = GPIO_PULLDOWN_DISABLE, .intr_type = GPIO_INTR_DISABLE }; gpio_config(&io); gpio_set_level(CONFIG_LCD_PIN_BAT_EN, 1); // Backlight io.pin_bit_mask = 1ULL << CONFIG_LCD_PIN_BL; gpio_config(&io); gpio_set_level(CONFIG_LCD_PIN_BL, 1); // Status LED (optional) io.pin_bit_mask = 1ULL << CONFIG_LCD_PIN_LED; gpio_config(&io); gpio_set_level(CONFIG_LCD_PIN_LED, 0); // Button gpio_config_t bi = { .pin_bit_mask = 1ULL << CONFIG_LCD_PIN_BUTTON, .mode = GPIO_MODE_INPUT, .pull_up_en = GPIO_PULLUP_ENABLE, .pull_down_en = GPIO_PULLDOWN_DISABLE, .intr_type = GPIO_INTR_NEGEDGE }; gpio_config(&bi); gpio_install_isr_service(0); gpio_isr_handler_add(CONFIG_LCD_PIN_BUTTON, button_isr, NULL); } static void tone_init(void) { ledc_timer_config_t tcfg = { .speed_mode = LEDC_LOW_SPEED_MODE, .duty_resolution = LEDC_TIMER_10_BIT, .timer_num = LEDC_TIMER_0, .freq_hz = 2000, .clk_cfg = LEDC_AUTO_CLK, }; ledc_timer_config(&tcfg); ledc_channel_config_t ccfg = { .gpio_num = CONFIG_LCD_PIN_SPEAKER, .speed_mode = LEDC_LOW_SPEED_MODE, .channel = LEDC_CHANNEL_0, .intr_type = LEDC_INTR_DISABLE, .timer_sel = LEDC_TIMER_0, .duty = 0, .hpoint = 0, .flags = {0}, }; ledc_channel_config(&ccfg); } static void tone_start(uint32_t freq_hz) { ledc_set_freq(LEDC_LOW_SPEED_MODE, LEDC_TIMER_0, freq_hz); ledc_set_duty(LEDC_LOW_SPEED_MODE, LEDC_CHANNEL_0, (1 << 9)); // ~50% of 10-bit ledc_update_duty(LEDC_LOW_SPEED_MODE, LEDC_CHANNEL_0); } static void tone_stop(void) { ledc_set_duty(LEDC_LOW_SPEED_MODE, LEDC_CHANNEL_0, 0); ledc_update_duty(LEDC_LOW_SPEED_MODE, LEDC_CHANNEL_0); } static void tone_beep(uint32_t freq_hz, uint32_t duration_ms) { tone_start(freq_hz); vTaskDelay(pdMS_TO_TICKS(duration_ms)); tone_stop(); } // LCD setup using esp_lcd static bool lcd_autoprobe(void) { ESP_LOGI(TAG, "LCD autoprobe start"); // Prepare SPI bus using SPI master driver (compatible across IDF versions) spi_bus_config_t buscfg = { .sclk_io_num = CONFIG_LCD_PIN_SCLK, .mosi_io_num = CONFIG_LCD_PIN_MOSI, .miso_io_num = -1, .quadwp_io_num = -1, .quadhd_io_num = -1, .max_transfer_sz = LCD_H_RES * 40 * sizeof(uint16_t), .flags = 0, }; ESP_ERROR_CHECK(spi_bus_initialize(SPI2_HOST, &buscfg, SPI_DMA_CH_AUTO)); const int clk_candidates[] = { 40000000, 32000000, 24000000 }; const int offsets_y[] = { 0, 20 }; for (size_t ck = 0; ck < sizeof(clk_candidates)/sizeof(clk_candidates[0]); ++ck) { for (int rot = 0; rot < 4; ++rot) { for (size_t oy = 0; oy < sizeof(offsets_y)/sizeof(offsets_y[0]); ++oy) { int offy = offsets_y[oy]; ESP_LOGI(TAG, "Probe try: clk=%d rot=%d offY=%d", clk_candidates[ck], rot, offy); esp_lcd_panel_io_spi_config_t io_cfg = { .dc_gpio_num = CONFIG_LCD_PIN_DC, .cs_gpio_num = CONFIG_LCD_PIN_CS, .pclk_hz = clk_candidates[ck], .lcd_cmd_bits = 8, .lcd_param_bits = 8, .spi_mode = 0, .trans_queue_depth = 10, }; esp_lcd_panel_io_handle_t io_handle = NULL; if (esp_lcd_new_panel_io_spi(SPI2_HOST, &io_cfg, &io_handle) != ESP_OK) { ESP_LOGW(TAG, "io new failed"); continue; } esp_lcd_panel_dev_config_t panel_cfg = { .reset_gpio_num = CONFIG_LCD_PIN_RST, .color_space = ESP_LCD_COLOR_SPACE_RGB, .bits_per_pixel = 16, .vendor_config = NULL, }; esp_lcd_panel_handle_t panel = NULL; if (esp_lcd_new_panel_st7789(io_handle, &panel_cfg, &panel) != ESP_OK) { ESP_LOGW(TAG, "panel new failed"); esp_lcd_panel_io_del(io_handle); continue; } if (esp_lcd_panel_reset(panel) != ESP_OK || esp_lcd_panel_init(panel) != ESP_OK) { ESP_LOGW(TAG, "panel init failed"); esp_lcd_panel_del(panel); esp_lcd_panel_io_del(io_handle); continue; } esp_lcd_panel_swap_xy(panel, rot & 1); esp_lcd_panel_mirror(panel, (rot & 2) != 0, (rot & 2) != 0); esp_lcd_panel_set_gap(panel, 0, offy); s_panel = panel; s_rotation = rot; s_offset_y = offy; s_spi_clk_hz = clk_candidates[ck]; color_sweep(); ESP_LOGI(TAG, "LCD autoprobe success: clk=%d rot=%d offY=%d", s_spi_clk_hz, s_rotation, s_offset_y); return true; } } } ESP_LOGE(TAG, "LCD autoprobe failed"); return false; } // Wi-Fi event group substitute via callbacks static bool s_got_ip = false; static void on_got_ip(void* arg, esp_event_base_t base, int32_t id, void* data) { ip_event_got_ip_t *e = (ip_event_got_ip_t *)data; ESP_LOGI(TAG, "Got IP: " IPSTR, IP2STR(&e->ip_info.ip)); s_got_ip = true; } static esp_err_t wifi_init_and_connect(const char *ssid, const char *pass, int timeout_ms) { ESP_ERROR_CHECK(nvs_flash_init()); ESP_ERROR_CHECK(esp_netif_init()); ESP_ERROR_CHECK(esp_event_loop_create_default()); esp_netif_create_default_wifi_sta(); wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT(); ESP_ERROR_CHECK(esp_wifi_init(&cfg)); ESP_ERROR_CHECK(esp_event_handler_register(IP_EVENT, IP_EVENT_STA_GOT_IP, &on_got_ip, NULL)); wifi_config_t wcfg = {0}; strlcpy((char*)wcfg.sta.ssid, ssid ? ssid : "", sizeof(wcfg.sta.ssid)); strlcpy((char*)wcfg.sta.password, pass ? pass : "", sizeof(wcfg.sta.password)); wcfg.sta.threshold.authmode = WIFI_AUTH_WPA2_PSK; wcfg.sta.pmf_cfg.capable = true; wcfg.sta.pmf_cfg.required = false; ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA)); ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &wcfg)); ESP_ERROR_CHECK(esp_wifi_start()); ESP_ERROR_CHECK(esp_wifi_connect()); const int64_t start = esp_timer_get_time(); int backoff_ms = 500; while (!s_got_ip) { if ((esp_timer_get_time() - start) / 1000 > timeout_ms) { ESP_LOGE(TAG, "Wi-Fi connect timeout"); return ESP_ERR_TIMEOUT; } vTaskDelay(pdMS_TO_TICKS(backoff_ms)); backoff_ms = backoff_ms < 5000 ? backoff_ms * 2 : 5000; } return ESP_OK; } // HTTP client event: accumulate body typedef struct { char *buf; size_t len; } http_buf_t; static esp_err_t http_evt_hdl(esp_http_client_event_t *evt) { http_buf_t *hb = (http_buf_t *)evt->user_data; switch (evt->event_id) { case HTTP_EVENT_ON_DATA: if (evt->data_len > 0) { char *p = realloc(hb->buf, hb->len + evt->data_len + 1); if (!p) return ESP_FAIL; hb->buf = p; memcpy(hb->buf + hb->len, evt->data, evt->data_len); hb->len += evt->data_len; hb->buf[hb->len] = '\0'; } break; default: break; } return ESP_OK; } static char *json_escape(const char *s) { size_t n = 0; for (const char *p = s; *p; ++p) { switch (*p) { case '"': case '\\': case '\n': case '\r': case '\t': n += 2; break; default: n += 1; } } char *out = malloc(n + 1); char *w = out; for (const char *p = s; *p; ++p) { switch (*p) { case '"': *w++ = '\\'; *w++ = '"'; break; case '\\': *w++ = '\\'; *w++ = '\\'; break; case '\n': *w++ = '\\'; *w++ = 'n'; break; case '\r': *w++ = '\\'; *w++ = 'r'; break; case '\t': *w++ = '\\'; *w++ = 't'; break; default: *w++ = *p; break; } } *w = '\0'; return out; } static char *build_gemini_payload(const char *prompt) { char *e = json_escape(prompt); const char *fmt = "{\"contents\":[{\"parts\":[{\"text\":\"%s\"}]}],\"generationConfig\":{\"maxOutputTokens\":150,\"temperature\":0.7,\"topP\":0.8,\"topK\":40}}"; size_t len = strlen(fmt) + strlen(e) + 1; char *buf = malloc(len); snprintf(buf, len, fmt, e); free(e); return buf; } static char *parse_gemini_reply(const char *json) { cJSON *root = cJSON_Parse(json); if (!root) { ESP_LOGE(TAG, "JSON parse error: %s", cJSON_GetErrorPtr()); return NULL; } cJSON *candidates = cJSON_GetObjectItemCaseSensitive(root, "candidates"); if (!cJSON_IsArray(candidates) || cJSON_GetArraySize(candidates) == 0) { ESP_LOGE(TAG, "No candidates found in JSON"); cJSON_Delete(root); return NULL; } cJSON *first_candidate = cJSON_GetArrayItem(candidates, 0); cJSON *content = cJSON_GetObjectItemCaseSensitive(first_candidate, "content"); cJSON *parts = cJSON_GetObjectItemCaseSensitive(content, "parts"); if (!cJSON_IsArray(parts) || cJSON_GetArraySize(parts) == 0) { ESP_LOGE(TAG, "No parts found in content"); cJSON_Delete(root); return NULL; } cJSON *first_part = cJSON_GetArrayItem(parts, 0); cJSON *text = cJSON_GetObjectItemCaseSensitive(first_part, "text"); char *reply_text = NULL; if (cJSON_IsString(text) && (text->valuestring != NULL)) { reply_text = strdup(text->valuestring); } else { ESP_LOGE(TAG, "Text field not found or not a string"); } cJSON_Delete(root); return reply_text; } static char *gemini_generate_text(const char *api_key, const char *prompt, int *http_status_out) { if (!api_key || strlen(api_key) == 0) { ESP_LOGE(TAG, "API key not set"); return NULL; } char url[256]; snprintf(url, sizeof(url), "https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash:generateContent?key=%s", api_key); http_buf_t hb = {0}; esp_http_client_config_t cfg = { .url = url, .event_handler = http_evt_hdl, .user_data = &hb, .method = HTTP_METHOD_POST, #ifdef HAS_CRT_BUNDLE .crt_bundle_attach = esp_crt_bundle_attach, #endif .timeout_ms = 15000, }; esp_http_client_handle_t cli = esp_http_client_init(&cfg); if (!cli) return NULL; char *payload = build_gemini_payload(prompt); esp_http_client_set_header(cli, "Content-Type", "application/json"); esp_http_client_set_post_field(cli, payload, strlen(payload)); esp_err_t err = esp_http_client_perform(cli); int status = -1; if (err == ESP_OK) { status = esp_http_client_get_status_code(cli); ESP_LOGI(TAG, "HTTP status: %d, len=%d", status, (int)hb.len); } else { ESP_LOGE(TAG, "HTTP perform failed: %s", esp_err_to_name(err)); } if (http_status_out) *http_status_out = status; char *reply_text = NULL; if (status == 200 && hb.buf) { reply_text = parse_gemini_reply(hb.buf); } free(payload); if (hb.buf) free(hb.buf); esp_http_client_cleanup(cli); return reply_text; } static void chat_cycle_task(void *arg) { while (1) { if (xSemaphoreTake(s_btn_sem, portMAX_DELAY) == pdTRUE) { ui_set_state(UI_LISTENING); tone_start(880); vTaskDelay(pdMS_TO_TICKS(5000)); // simulate listening tone_stop(); ui_set_state(UI_THINKING); tone_beep(1400, 150); int http_status = -1; char *reply = gemini_generate_text(CONFIG_APP_GEMINI_API_KEY, "Say a friendly hello from ESP32-C6.", &http_status); if (!reply) { ESP_LOGE(TAG, "Gemini request failed (status=%d)", http_status); ui_set_state(UI_ERROR); vTaskDelay(pdMS_TO_TICKS(1000)); ui_set_state(UI_READY); continue; } ui_set_state(UI_SPEAKING); ESP_LOGI(TAG, "Gemini reply: %s", reply); tone_start(660); vTaskDelay(pdMS_TO_TICKS(1500)); tone_stop(); free(reply); ui_set_state(UI_READY); } } } void app_main(void) { s_btn_sem = xSemaphoreCreateBinary(); gpio_init_all(); tone_init(); ui_set_state(UI_BOOT); if (!lcd_autoprobe()) { ui_set_state(UI_ERROR); return; } ui_set_state(UI_CONNECTING); if (wifi_init_and_connect(CONFIG_APP_WIFI_SSID, CONFIG_APP_WIFI_PASSWORD, 30000) != ESP_OK) { ui_set_state(UI_ERROR); return; } ui_set_state(UI_READY); xTaskCreate(chat_cycle_task, "chat", 8192, NULL, 5, NULL); }