//
// ESP32Support.c
//
//
// Created by Chris Galzerano on 2/7/26.
//
#include "ESP32Support.h"
#define MINIMP3_IMPLEMENTATION
#include "minimp3.h"
#include "FilesApp.h"
#include "ClockApp.h"
#include "PaintApp.h"
#include "MusicApp.h"
wl_handle_t wl_handle = WL_INVALID_HANDLE;
// Pin Definitions SPI LCD
#define PIN_NUM_MOSI 11
#define PIN_NUM_CLK 12
#define PIN_NUM_CS 10
#define PIN_NUM_DC 9
#define PIN_NUM_RST 8
#define PIN_NUM_BK_LIGHT -1 // Use a real GPIO if you have a backlight control pin
// --- NEW, CORRECTED PIN CONFIGURATION ---
#define I2C_HOST I2C_NUM_0
#define PIN_NUM_TOUCH_RST 4 // Your RST pin
#define PIN_NUM_TOUCH_INT 5 // Your INT pin
#define PIN_NUM_I2C_SDA 6 // Your SDA pin
#define PIN_NUM_I2C_SCL 7 // Your SCL pin
// --- Configuration for ILI9488 ---
#if defined(USE_ILI9488)
#define LCD_PIXEL_CLOCK_HZ (30 * 1000 * 1000)
#define LCD_H_RES 320
#define LCD_V_RES 480
#define LCD_COLOR_SPACE ESP_LCD_COLOR_SPACE_RGB // ILI9488 is typically RGB
#define LCD_DRIVER_FN() esp_lcd_new_panel_ili9488(io_handle, &panel_config, &panel_handle)
//RGB Display Pinout
#define LCD_COLOR_BYTES_PER_PIXEL 2 // Using 16-bit RGB565 for simpler pinout
#define LCD_INIT_BUFFER_SIZE 2048 // 2KB is usually more than enough for init commands
#define FRAME_BUFFER_SIZE (LCD_H_RES * LCD_V_RES * 2)
// --- GPIO PIN DEFINITIONS ---
// 1. Pins Freed for I2S/SDIO (DO NOT USE THESE BELOW)
// I2S: GPIO 40, 41, 42 | SDIO: GPIO 1, 5, 6, 7, 8, 9
// We will define them later, but they are reserved here.
// --- 1. I2S Audio Pin Definitions (FINAL MOVE) ---
#define I2S_PIN_NUM_BCLK 1
#define I2S_PIN_NUM_LRCK 2
#define I2S_PIN_NUM_DOUT 46 // Moved to 46 to free up 3 and 4
// --- 2. SD Card (SDIO 4-bit) Pin Definitions ---
#define SDIO_PIN_NUM_CLK 5
#define SDIO_PIN_NUM_CMD 6
#define SDIO_PIN_NUM_D0 7
#define SDIO_PIN_NUM_D1 8
#define SDIO_PIN_NUM_D2 9
#define SDIO_PIN_NUM_D3 10
// --- 3. SPI Pins for ILI9488 Initialization ---
#define LCD_PIN_NUM_SPI_CS 11
#define LCD_PIN_NUM_SPI_SCLK 12
#define LCD_PIN_NUM_SPI_MOSI 13
#define LCD_PIN_NUM_SPI_DC 10 // Safest choice for Data/Command
// --- 4. General Control Pin Definitions ---
#define LCD_PIN_NUM_RST 14
#define LCD_PIN_NUM_BK_LIGHT 15
// --- 5. RGB Interface Pins for DISPLAY (SYNC LINES) ---
#define LCD_PIN_NUM_PCLK 1
#define LCD_PIN_NUM_HSYNC 0
#define LCD_PIN_NUM_VSYNC 45
#define LCD_PIN_NUM_DE 47
// --- 6. RGB Data Bus (D0-D15) - FINAL, VERIFIED PINS ---
// Low Byte (D0-D7)
#define LCD_PIN_NUM_D0 20
#define LCD_PIN_NUM_D1 21
#define LCD_PIN_NUM_D2 16 // Remapped from 23
#define LCD_PIN_NUM_D3 17 // Remapped from 25
#define LCD_PIN_NUM_D4 18 // Remapped from 26
#define LCD_PIN_NUM_D5 19 // Remapped from 27
#define LCD_PIN_NUM_D6 4 // Remapped from 33 (New location!)
#define LCD_PIN_NUM_D7 3 // Remapped from 34 (New location!)
// High Byte (D8-D15)
#define LCD_PIN_NUM_D8 7
#define LCD_PIN_NUM_D9 8
#define LCD_PIN_NUM_D10 9
#define LCD_PIN_NUM_D11 38
#define LCD_PIN_NUM_D12 39
#define LCD_PIN_NUM_D13 40
#define LCD_PIN_NUM_D14 41
#define LCD_PIN_NUM_D15 42
// --- 6. RGB Data Bus (D16, D17) - NEW 18-BIT LINES ---
#define LCD_PIN_NUM_D16 5 // LCD Pin: D16
#define LCD_PIN_NUM_D17 6 // LCD Pin: D17
// NOTE: Please carefully check your specific ESP32-S3 module's pinout.
// GPIOs 22, 24, and 28-32 are often not available. The example above is
// adjusted to skip these unavailable pins, creating the 16-pin bus.
// IMPORTANT: LCD timing parameters for ILI9488
// These are typical values, but you MUST verify them with your datasheet.
#define LCD_PCLK_HZ (6 * 1000 * 1000) // 6MHz for 480x320
// Increase the timing margins (Porches)
#define LCD_HSYNC_PULSE_WIDTH 5
#define LCD_HSYNC_BACK_PORCH 20 // Increased from 8
#define LCD_HSYNC_FRONT_PORCH 20 // Increased from 10
#define LCD_VSYNC_PULSE_WIDTH 5
#define LCD_VSYNC_BACK_PORCH 20 // Increased from 8
#define LCD_VSYNC_FRONT_PORCH 20 // Increased from 10
// --- Global Variable Definitions ---
int32_t g_last_x = 0;
int32_t g_last_y = 0;
lv_indev_state_t g_last_state = LV_INDEV_STATE_REL; // Start as Released
SemaphoreHandle_t g_touch_mutex = NULL; // Initialize to NULL or create in init()
const int DISPLAY_HORIZONTAL_PIXELS = 320;
const int DISPLAY_VERTICAL_PIXELS = 480;
const int DISPLAY_COMMAND_BITS = 8;
const int DISPLAY_PARAMETER_BITS = 8;
const unsigned int DISPLAY_REFRESH_HZ = 40000000;
const int DISPLAY_SPI_QUEUE_LEN = 10;
const int SPI_MAX_TRANSFER_SIZE = 32768;
// Note: You can switch this logic based on your build flags as before
const lcd_rgb_element_order_t TFT_COLOR_MODE = COLOR_RGB_ELEMENT_ORDER_RGB;
// Default to 25 lines of color data
const size_t LV_BUFFER_SIZE = 320 * 25; // DISPLAY_HORIZONTAL_PIXELS * 25
const int LVGL_UPDATE_PERIOD_MS = 5;
/*
const ledc_mode_t BACKLIGHT_LEDC_MODE = LEDC_LOW_SPEED_MODE;
const ledc_channel_t BACKLIGHT_LEDC_CHANNEL = LEDC_CHANNEL_0;
const ledc_timer_t BACKLIGHT_LEDC_TIMER = LEDC_TIMER_1;
const ledc_timer_bit_t BACKLIGHT_LEDC_TIMER_RESOLUTION = LEDC_TIMER_10_BIT;
const uint32_t BACKLIGHT_LEDC_FRQUENCY = 5000;
*/
esp_lcd_panel_io_handle_t lcd_io_handle = NULL;
esp_lcd_panel_handle_t lcd_handle = NULL;
lv_draw_buf_t lv_disp_buf;
lv_display_t *lv_display = NULL;
lv_color_t *lv_buf_1 = NULL;
lv_color_t *lv_buf_2 = NULL;
lv_obj_t *meter = NULL;
lv_style_t style_screen;
// Place this outside of app_main
void lcd_cmd(spi_device_handle_t spi, const uint8_t cmd)
{
// Ensure DC is LOW for command (0)
gpio_set_level(LCD_PIN_NUM_SPI_DC, 0);
// Create the transaction configuration
spi_transaction_t t = {
.length = 8, // 8 bits long
.tx_buffer = &cmd,
};
// Send the command
spi_device_transmit(spi, &t);
}
void lcd_data(spi_device_handle_t spi, const uint8_t *data, int len)
{
if (len == 0) return;
// Ensure DC is HIGH for data (1)
gpio_set_level(LCD_PIN_NUM_SPI_DC, 1);
// Create the transaction configuration
spi_transaction_t t = {
.length = len * 8, // length is in bits
.tx_buffer = data,
};
// Send the data
spi_device_transmit(spi, &t);
}
// Function to send a single command followed by a single parameter
void ili9488_set_colmod(spi_device_handle_t spi)
{
const uint8_t cmd_colmod = 0x3A; // Command to set pixel format
const uint8_t param_rgb565 = 0x55; // Parameter for 16-bit (RGB565)
// 1. Send the command (DC=0)
lcd_cmd(spi, cmd_colmod);
// 2. Send the parameter (DC=1)
lcd_data(spi, ¶m_rgb565, 1);
}
// --- ILI9488 Command Definitions (MUST be visible to the array) ---
#define ILI9488_SWRESET 0x01
#define ILI9488_SLPOUT 0x11
#define ILI9488_DISPON 0x29
#define ILI9488_PIXFMT 0x3A
#define ILI9488_FRMCTR1 0xB1
#define ILI9488_INVCTR 0xB4
#define ILI9488_DFUNCTR 0xB6
#define ILI9488_PWCTR1 0xC0
#define ILI9488_PWCTR2 0xC1
#define ILI9488_VMCTR1 0xC5
#define ILI9488_GMCTRP1 0xE0
#define ILI9488_GMCTRN1 0xE1
#define ILI9488_IMGFUNCT 0xE9
#define ILI9488_ADJCTR3 0xF7
// Commands used in your array (from the STM32 source):
#define ILI9488_GMCTRP1 0xE0
#define ILI9488_GMCTRN1 0xE1
#define ILI9488_PWCTR1 0xC0
#define ILI9488_PWCTR2 0xC1
#define ILI9488_VMCTR1 0xC5
#define ILI9488_PIXFMT 0x3A
#define ILI9488_FRMCTR1 0xB1
#define ILI9488_INVCTR 0xB4
#define ILI9488_DFUNCTR 0xB6
#define ILI9488_IMGFUNCT 0xE9
#define ILI9488_ADJCTR3 0xF7
#define ILI9488_SLPOUT 0x11
#define ILI9488_DISPON 0x29
#define ILI9488_SWRESET 0x01
// Adapt the command list from the STM32 driver (ili9488_Init function)
static const uint8_t ili9488_rgb_init_sequence[][20] = {
// Command, Parameter1, Parameter2, ...
{ ILI9488_SWRESET, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // 0x01: SWRESET (No params, but needs delay)
{ ILI9488_GMCTRP1, 0x00, 0x01, 0x02, 0x04, 0x14, 0x09, 0x3F, 0x57, 0x4D, 0x05, 0x0B, 0x09, 0x1A, 0x1D, 0x0F }, // 0xE0: Positive Gamma Control (15 params)
{ ILI9488_GMCTRN1, 0x00, 0x1D, 0x20, 0x02, 0x0E, 0x03, 0x35, 0x12, 0x47, 0x02, 0x0D, 0x0C, 0x38, 0x39, 0x0F }, // 0xE1: Negative Gamma Control (15 params)
{ ILI9488_PWCTR1, 0x17, 0x15, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // 0xC0: Power Control 1 (2 params)
{ ILI9488_PWCTR2, 0x41, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // 0xC1: Power Control 2 (1 param)
{ ILI9488_VMCTR1, 0x00, 0x12, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // 0xC5: VCOM Control (3 params)
// *** CRITICAL CHANGE ***
// 0x3A: PIXFMT. Must be 0x55 for 16-bit RGB565.
{ ILI9488_PIXFMT, 0x55, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // 0x3A: Interface Pixel Format (1 param)
// 0xB0: Interface Mode Control. We need to set the parallel mode bits, which is NOT 0x80/0x00.
// The ILI9488 requires different commands for DPI (Parallel RGB) vs. MCU (8080/DPI) mode.
// We will use 0x02 (which enables the DPI) or 0x03 (for 16-bit DPI). This depends on hardware. Let's try 0x00 (the standard reset state)
// and rely on the RGB panel to set the mode, but it might need 0x02.
// Since the driver failed to set this, let's skip it and focus on the power-up sequence.
{ ILI9488_FRMCTR1, 0xA0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // 0xB1: Frame rate (1 param)
{ ILI9488_INVCTR, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // 0xB4: Display Inversion Control (1 param)
{ ILI9488_DFUNCTR, 0x02, 0x02, 0x3B, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // 0xB6: Display Function Control (3 params)
{ ILI9488_IMGFUNCT, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // 0xE9: Set Image Function (1 param)
{ ILI9488_ADJCTR3, 0xA9, 0x51, 0x2C, 0x82, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // 0xF7: Adjust Control (4 params)
// **Final Wake-up**
{ ILI9488_SLPOUT, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // 0x11: Exit Sleep (Needs 120ms delay)
{ ILI9488_DISPON, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // 0x29: Display ON (Needs 5ms delay)
};
void initRgbDisplay(void) {
// --- Part 1: Initialize Backlight ---
FreeOSLogI(TAG, "Initializing LCD backlight on GPIO %d...", LCD_PIN_NUM_BK_LIGHT);
// ... (Backlight setup code remains the same)
gpio_config_t bk_gpio_conf = {
.pin_bit_mask = 1ULL << LCD_PIN_NUM_BK_LIGHT,
.mode = GPIO_MODE_OUTPUT,
};
ESP_ERROR_CHECK(gpio_config(&bk_gpio_conf));
gpio_set_level(LCD_PIN_NUM_BK_LIGHT, 1);
// --- Part 2: Initialize ILI9488 via temporary SPI bus ---
FreeOSLogI(TAG, "Installing temporary SPI bus for ILI9488 init...");
spi_bus_config_t buscfg = {
.mosi_io_num = LCD_PIN_NUM_SPI_MOSI,
.sclk_io_num = LCD_PIN_NUM_SPI_SCLK,
.miso_io_num = -1,
.quadwp_io_num = -1,
.quadhd_io_num = -1,
.max_transfer_sz = LCD_H_RES * 10, // Small transfer size for commands
};
ESP_ERROR_CHECK(spi_bus_initialize(SPI2_HOST, &buscfg, SPI_DMA_CH_AUTO));
esp_lcd_panel_io_handle_t io_handle = NULL;
esp_lcd_panel_io_spi_config_t io_config = {
.cs_gpio_num = LCD_PIN_NUM_SPI_CS,
.dc_gpio_num = LCD_PIN_NUM_SPI_DC,
.spi_mode = 0,
.pclk_hz = 10 * 1000 * 1000,
.trans_queue_depth = 10,
.lcd_cmd_bits = 8,
.lcd_param_bits = 8,
};
ESP_ERROR_CHECK(esp_lcd_new_panel_io_spi((esp_lcd_spi_bus_handle_t)SPI2_HOST, &io_config, &io_handle));
FreeOSLogI(TAG, "Installing ILI9488 driver over SPI...");
esp_lcd_panel_handle_t init_panel_handle = NULL;
esp_lcd_panel_dev_config_t panel_config = {
.reset_gpio_num = LCD_PIN_NUM_RST,
.bits_per_pixel = 16,
};
ESP_ERROR_CHECK(esp_lcd_new_panel_ili9488(io_handle,
&panel_config,
LCD_INIT_BUFFER_SIZE, // <--- NEW ARGUMENT
&init_panel_handle));
FreeOSLogI(TAG, "Resetting and Initializing ILI9488...");
ESP_ERROR_CHECK(esp_lcd_panel_reset(init_panel_handle));
ESP_ERROR_CHECK(esp_lcd_panel_init(init_panel_handle));
// Optional: Set the ILI9488 to accept RGB565 (16-bit) data
// Command 0x3A is COLMOD (Pixel Format). 0x55 is 16-bit (RGB565).
esp_lcd_panel_io_tx_param(io_handle, 0x3A, (uint8_t[]){0x55}, 1);
// Disable SPI interface
FreeOSLogI(TAG, "Initialization complete. Deleting SPI handles...");
ESP_ERROR_CHECK(esp_lcd_panel_del(init_panel_handle));
ESP_ERROR_CHECK(esp_lcd_panel_io_del(io_handle));
ESP_ERROR_CHECK(spi_bus_free(SPI2_HOST));
// --- Part 3: Install the permanent RGB interface ---
FreeOSLogI(TAG, "Installing RGB panel driver for ILI9488...");
esp_lcd_rgb_panel_config_t rgb_config = {
.clk_src = LCD_CLK_SRC_DEFAULT,
.disp_gpio_num = -1,
.psram_trans_align = 64,
.data_width = 16, // Use 16-bit bus
.bits_per_pixel = 16,
.de_gpio_num = LCD_PIN_NUM_DE,
.pclk_gpio_num = LCD_PIN_NUM_PCLK,
.vsync_gpio_num = LCD_PIN_NUM_VSYNC,
.hsync_gpio_num = LCD_PIN_NUM_HSYNC,
.data_gpio_nums = {
LCD_PIN_NUM_D0, LCD_PIN_NUM_D1, LCD_PIN_NUM_D2, LCD_PIN_NUM_D3,
LCD_PIN_NUM_D4, LCD_PIN_NUM_D5, LCD_PIN_NUM_D6, LCD_PIN_NUM_D7,
LCD_PIN_NUM_D8, LCD_PIN_NUM_D9, LCD_PIN_NUM_D10, LCD_PIN_NUM_D11,
LCD_PIN_NUM_D12, LCD_PIN_NUM_D13, LCD_PIN_NUM_D14, LCD_PIN_NUM_D15,
},
.timings = {
.pclk_hz = LCD_PCLK_HZ,
.h_res = LCD_H_RES,
.v_res = LCD_V_RES,
.flags.pclk_active_neg = true, // TRY THIS AGAIN! Inverting the clock edge
.flags.vsync_idle_low = false, // Standard for VSYNC (try swapping if needed)
.flags.hsync_idle_low = false, // Standard for HSYNC (try swapping if needed)
.hsync_pulse_width = LCD_HSYNC_PULSE_WIDTH,
.hsync_back_porch = LCD_HSYNC_BACK_PORCH,
.hsync_front_porch = LCD_HSYNC_FRONT_PORCH,
.vsync_pulse_width = LCD_VSYNC_PULSE_WIDTH,
.vsync_back_porch = LCD_VSYNC_BACK_PORCH,
.vsync_front_porch = LCD_VSYNC_FRONT_PORCH,
},
.flags.fb_in_psram = true,
};
esp_lcd_panel_handle_t rgb_panel_handle = NULL;
ESP_ERROR_CHECK(esp_lcd_new_rgb_panel(&rgb_config, &rgb_panel_handle));
ESP_ERROR_CHECK(esp_lcd_panel_init(rgb_panel_handle));
// Set the ILI9488 to accept RGB565 (16-bit) data
// Command 0x3A is COLMOD (Pixel Format). 0x55 is 16-bit (RGB565).
//esp_lcd_panel_io_tx_param(io_handle, 0x3A, (uint8_t[]){0x55}, 1);
//ESP_ERROR_CHECK(esp_lcd_panel_io_tx_param(io_handle, 0x29, NULL, 0));
// Inside app_main, before calling esp_lcd_new_panel_ili9488:
/// --- Part 2: Initialize ILI9488 via temporary SPI bus (Corrected Order) ---
// 1. Define the physical bus GPIOs
spi_bus_config_t buscfg1 = {
.mosi_io_num = LCD_PIN_NUM_SPI_MOSI,
.sclk_io_num = LCD_PIN_NUM_SPI_SCLK,
.miso_io_num = -1, // Not needed for write-only
.quadwp_io_num = -1,
.quadhd_io_num = -1,
.max_transfer_sz = 1024,
};
// Line 1: Initialize the physical bus and pins (This was missing or misplaced!)
FreeOSLogI(TAG, "Initializing SPI bus host...");
ESP_ERROR_CHECK(spi_bus_initialize(SPI2_HOST, &buscfg1, SPI_DMA_CH_AUTO));
// 2. Define the device configuration
spi_device_handle_t raw_spi_handle = NULL;
spi_device_interface_config_t devcfg={
.clock_speed_hz = 10 * 1000 * 1000,
.mode = 0,
.spics_io_num = LCD_PIN_NUM_SPI_CS,
.queue_size = 7,
.pre_cb = NULL,
};
// Line 2: Add the device to the now-initialized bus
FreeOSLogI(TAG, "Adding ILI9488 device to bus...");
ESP_ERROR_CHECK(spi_bus_add_device(SPI2_HOST, &devcfg, &raw_spi_handle)); // Your crash line (now fixed)
// 3. Manually send the required commands
// Ensure the DC pin (GPIO 8) is configured as output before this line!
gpio_set_direction(LCD_PIN_NUM_SPI_DC, GPIO_MODE_OUTPUT);
FreeOSLogI(TAG, "Manually setting 16-bit color format...");
ili9488_set_colmod(raw_spi_handle); // This command should now execute!
// ... continue with esp_lcd_new_panel_ili9488 call, etc.
// Manually run the hardware reset (already working, but cleaner here)
// Assuming GPIO 14 is your reset pin
gpio_set_level(LCD_PIN_NUM_RST, 0);
vTaskDelay(pdMS_TO_TICKS(10));
gpio_set_level(LCD_PIN_NUM_RST, 1);
vTaskDelay(pdMS_TO_TICKS(120));
FreeOSLogI(TAG, "Starting full ILI9488 init sequence...");
for (int i = 0; i < sizeof(ili9488_rgb_init_sequence) / sizeof(ili9488_rgb_init_sequence[0]); i++) {
uint8_t cmd = ili9488_rgb_init_sequence[i][0];
const uint8_t *data = &ili9488_rgb_init_sequence[i][1];
// Calculate length based on known commands or hardcoding
int len = 0;
if (cmd == ILI9488_GMCTRP1 || cmd == ILI9488_GMCTRN1) len = 15;
else if (cmd == ILI9488_PWCTR1) len = 2;
else if (cmd == ILI9488_VMCTR1) len = 3;
else if (cmd == ILI9488_ADJCTR3) len = 4;
else if (cmd == ILI9488_DFUNCTR) len = 2; // Adjusted from 3 to 2 based on pattern
else if (cmd == ILI9488_PWCTR2 || cmd == ILI9488_PIXFMT || cmd == ILI9488_FRMCTR1 || cmd == ILI9488_INVCTR || cmd == ILI9488_IMGFUNCT) len = 1;
if (cmd == ILI9488_SWRESET) {
lcd_cmd(raw_spi_handle, cmd);
vTaskDelay(pdMS_TO_TICKS(5));
} else if (cmd == ILI9488_SLPOUT) {
lcd_cmd(raw_spi_handle, cmd);
vTaskDelay(pdMS_TO_TICKS(120));
} else if (cmd == ILI9488_DISPON) {
lcd_cmd(raw_spi_handle, cmd);
vTaskDelay(pdMS_TO_TICKS(5));
} else {
lcd_cmd(raw_spi_handle, cmd);
if (len > 0) {
lcd_data(raw_spi_handle, data, len);
}
}
}
FreeOSLogI(TAG, "ILI9488 is now configured.");
// ... (Proceed to delete SPI handles and initialize esp_lcd_new_panel_rgb) ...
// --- Part 4: Draw to the "live" framebuffer ---
uint16_t *framebuffer1 = NULL;
ESP_ERROR_CHECK(esp_lcd_rgb_panel_get_frame_buffer(rgb_panel_handle, 1, (void **)&framebuffer1));
FreeOSLogI(TAG, "Drawing solid green to the %dx%d framebuffer...", LCD_H_RES, LCD_V_RES);
uint16_t green_color = 0x07E0; // RGB565: 00000 111111 00000 (Pure Green)
for (int i = 0; i < LCD_H_RES * LCD_V_RES; i++) {
framebuffer1[i] = green_color;
}
FreeOSLogI(TAG, "Display should be green and refreshed by hardware.");
}
// Add this near the top of your file
static void lvgl_flush_cb(lv_display_t *disp, const lv_area_t *area, uint8_t *px_map);
/**
* @brief DMA callback for esp_lcd.
* This function is called from an ISR when the SPI transfer is done.
*/
static bool notify_lvgl_flush_ready(esp_lcd_panel_io_handle_t panel_io,
esp_lcd_panel_io_event_data_t *edata,
void *user_ctx)
{
// user_ctx is the address of the global lv_display pointer (&lv_display)
lv_display_t **disp_ptr = (lv_display_t **)user_ctx;
lv_display_t *disp = *disp_ptr;
// Check if LVGL has created the display object yet
if (disp) {
// Tell LVGL that the buffer flushing is complete
lv_display_flush_ready(disp);
}
// Return false, indicating no high-priority task was woken.
return false;
}
static void lvgl_flush_cb(lv_display_t *disp, const lv_area_t *area, uint8_t *px_map) {
esp_lcd_panel_handle_t panel_handle = (esp_lcd_panel_handle_t)lv_display_get_user_data(disp);
int offsetx1 = area->x1;
int offsetx2 = area->x2;
int offsety1 = area->y1;
int offsety2 = area->y2;
// Change line 165 to this:
esp_lcd_panel_draw_bitmap(panel_handle, offsetx1, offsety1, offsetx2 + 1, offsety2 + 1, px_map);
// At the very end, tell LVGL you are done
lv_display_flush_ready(disp);
}
static void IRAM_ATTR lvgl_tick_cb(void *param)
{
lv_tick_inc(LVGL_UPDATE_PERIOD_MS);
}
/*static void display_brightness_init(void)
{
const ledc_channel_config_t LCD_backlight_channel =
{
.gpio_num = GPIO_NUM_NC,
.speed_mode = BACKLIGHT_LEDC_MODE,
.channel = BACKLIGHT_LEDC_CHANNEL,
.intr_type = LEDC_INTR_DISABLE,
.timer_sel = BACKLIGHT_LEDC_TIMER,
.duty = 0,
.hpoint = 0,
.flags =
{
.output_invert = 0
}
};
const ledc_timer_config_t LCD_backlight_timer =
{
.speed_mode = BACKLIGHT_LEDC_MODE,
.duty_resolution = BACKLIGHT_LEDC_TIMER_RESOLUTION,
.timer_num = BACKLIGHT_LEDC_TIMER,
.freq_hz = BACKLIGHT_LEDC_FRQUENCY,
.clk_cfg = LEDC_AUTO_CLK
};
FreeOSLogI(TAG, "Initializing LEDC for backlight pin: %d", GPIO_NUM_NC);
ESP_ERROR_CHECK(ledc_timer_config(&LCD_backlight_timer));
ESP_ERROR_CHECK(ledc_channel_config(&LCD_backlight_channel));
}
void display_brightness_set(int brightness_percentage)
{
if (brightness_percentage > 100)
{
brightness_percentage = 100;
}
else if (brightness_percentage < 0)
{
brightness_percentage = 0;
}
FreeOSLogI(TAG, "Setting backlight to %d%%", brightness_percentage);
// LEDC resolution set to 10bits, thus: 100% = 1023
uint32_t duty_cycle = (1023 * brightness_percentage) / 100;
ESP_ERROR_CHECK(ledc_set_duty(BACKLIGHT_LEDC_MODE, BACKLIGHT_LEDC_CHANNEL, duty_cycle));
ESP_ERROR_CHECK(ledc_update_duty(BACKLIGHT_LEDC_MODE, BACKLIGHT_LEDC_CHANNEL));
}*/
void initialize_spi()
{
FreeOSLogI(TAG, "Initializing SPI bus (MOSI:%d, MISO:%d, CLK:%d)",
PIN_NUM_MOSI, GPIO_NUM_NC, PIN_NUM_CLK);
spi_bus_config_t bus =
{
.mosi_io_num = PIN_NUM_MOSI,
.miso_io_num = GPIO_NUM_NC,
.sclk_io_num = PIN_NUM_CLK,
.quadwp_io_num = GPIO_NUM_NC,
.quadhd_io_num = GPIO_NUM_NC,
.data4_io_num = GPIO_NUM_NC,
.data5_io_num = GPIO_NUM_NC,
.data6_io_num = GPIO_NUM_NC,
.data7_io_num = GPIO_NUM_NC,
.max_transfer_sz = SPI_MAX_TRANSFER_SIZE,
.flags = SPICOMMON_BUSFLAG_SCLK |
SPICOMMON_BUSFLAG_MOSI | SPICOMMON_BUSFLAG_MASTER,
.intr_flags = ESP_INTR_FLAG_LOWMED | ESP_INTR_FLAG_IRAM
};
ESP_ERROR_CHECK(spi_bus_initialize(SPI2_HOST, &bus, SPI_DMA_CH_AUTO));
}
// The global semaphore flag
SemaphoreHandle_t lcd_flush_ready_sem = NULL;
// The new FreeOS callback to replace the LVGL one
static bool lcd_trans_done_cb(esp_lcd_panel_io_handle_t panel_io, esp_lcd_panel_io_event_data_t *edata, void *user_ctx) {
BaseType_t need_yield = pdFALSE;
if (lcd_flush_ready_sem) {
xSemaphoreGiveFromISR(lcd_flush_ready_sem, &need_yield);
}
return (need_yield == pdTRUE);
}
void initialize_display()
{
if (lcd_flush_ready_sem == NULL) {
lcd_flush_ready_sem = xSemaphoreCreateBinary();
xSemaphoreGive(lcd_flush_ready_sem); // Initialize it as "ready"
}
const esp_lcd_panel_io_spi_config_t io_config =
{
.cs_gpio_num = PIN_NUM_CS,
.dc_gpio_num = PIN_NUM_DC,
.spi_mode = 0,
.pclk_hz = DISPLAY_REFRESH_HZ,
.trans_queue_depth = DISPLAY_SPI_QUEUE_LEN,
.on_color_trans_done = lcd_trans_done_cb,
.user_ctx = NULL,
.lcd_cmd_bits = DISPLAY_COMMAND_BITS,
.lcd_param_bits = DISPLAY_PARAMETER_BITS,
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5,4,0)
.cs_ena_pretrans = 0,
.cs_ena_posttrans = 0,
#endif
.flags =
{
#if ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 0, 0)
.dc_as_cmd_phase = 0,
.dc_low_on_data = 0,
.octal_mode = 0,
.lsb_first = 0
#else
.dc_low_on_data = 0,
.octal_mode = 0,
.sio_mode = 0,
.lsb_first = 0,
.cs_high_active = 0
#endif
}
};
const esp_lcd_panel_dev_config_t lcd_config =
{
.reset_gpio_num = PIN_NUM_RST,
.color_space = TFT_COLOR_MODE,
.bits_per_pixel = 18,
.flags =
{
.reset_active_high = 0
},
.vendor_config = NULL
};
ESP_ERROR_CHECK(
esp_lcd_new_panel_io_spi((esp_lcd_spi_bus_handle_t)SPI2_HOST, &io_config, &lcd_io_handle));
ESP_ERROR_CHECK(esp_lcd_new_panel_ili9488(lcd_io_handle, &lcd_config, LV_BUFFER_SIZE, &lcd_handle));
ESP_ERROR_CHECK(esp_lcd_panel_reset(lcd_handle));
ESP_ERROR_CHECK(esp_lcd_panel_init(lcd_handle));
ESP_ERROR_CHECK(esp_lcd_panel_invert_color(lcd_handle, true));
ESP_ERROR_CHECK(esp_lcd_panel_swap_xy(lcd_handle, false));
ESP_ERROR_CHECK(esp_lcd_panel_mirror(lcd_handle, false, false));
ESP_ERROR_CHECK(esp_lcd_panel_set_gap(lcd_handle, 0, 0));
#if ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 0, 0)
ESP_ERROR_CHECK(esp_lcd_panel_disp_off(lcd_handle, false));
#else
ESP_ERROR_CHECK(esp_lcd_panel_disp_on_off(lcd_handle, true));
#endif
}
void initialize_lvgl()
{
FreeOSLogI(TAG, "Initializing LVGL");
lv_init();
FreeOSLogI(TAG, "Allocating LVGL draw buffers");
lv_buf_1 = (lv_color_t *)heap_caps_malloc(LV_BUFFER_SIZE * sizeof(lv_color_t), MALLOC_CAP_DMA);
assert(lv_buf_1);
#if USE_DOUBLE_BUFFERING
lv_buf_2 = (lv_color_t *)heap_caps_malloc(LV_BUFFER_SIZE * sizeof(lv_color_t), MALLOC_CAP_DMA);
assert(lv_buf_2);
#endif
FreeOSLogI(TAG, "Creating LVGL display");
// --- THIS IS THE NEW V9 DRIVER SETUP ---
// 1. Create a display
lv_display = lv_display_create(DISPLAY_HORIZONTAL_PIXELS, DISPLAY_VERTICAL_PIXELS);
lv_display_set_buffers(lv_display,
lv_buf_1,
lv_buf_2,
LV_BUFFER_SIZE,
LV_DISPLAY_RENDER_MODE_PARTIAL);
// 4. Set the flush callback
lv_display_set_flush_cb(lv_display, lvgl_flush_cb);
// 5. Pass your 'lcd_handle' (panel handle) to the display's user data
lv_display_set_user_data(lv_display, lcd_handle);
// --- END OF NEW V9 SETUP ---
FreeOSLogI(TAG, "Creating LVGL tick timer");
const esp_timer_create_args_t lvgl_tick_timer_args =
{
.callback = &lvgl_tick_cb,
.name = "lvgl_tick"
};
esp_timer_handle_t lvgl_tick_timer = NULL;
ESP_ERROR_CHECK(esp_timer_create(&lvgl_tick_timer_args, &lvgl_tick_timer));
ESP_ERROR_CHECK(esp_timer_start_periodic(lvgl_tick_timer, LVGL_UPDATE_PERIOD_MS * 1000));
}
static void lvgl_task_handler(void *pvParameters)
{
FreeOSLogI(TAG, "Starting LVGL task handler");
while (1)
{
// Run the LVGL timer handler
// This function will block for the appropriate amount of time
// based on the timers you have set up in LVGL.
lv_timer_handler();
// Delay for a short period to yield to other tasks.
// 10ms is a common value.
vTaskDelay(pdMS_TO_TICKS(10));
}
}
// --- Configuration for ST7789 ---
#elif defined(USE_ST7789)
#define LCD_PIXEL_CLOCK_HZ (40 * 1000 * 1000) // You can use 40MHz for ST7789 too
#define LCD_H_RES 320
#define LCD_V_RES 240
#define LCD_COLOR_SPACE ESP_LCD_COLOR_SPACE_BGR // ST7789 is often BGR
#define LCD_DRIVER_FN() esp_lcd_new_panel_st7789(io_handle, &panel_config, &panel_handle)
// --- Error if no display is selected ---
#else
#error "No display driver selected. Please define USE_ILI9488 or USE_ST7789."
#endif
#define LCD_COLOR_RED 0xF800 // RGB565 code for Red
// Global panel handle (needed for the drawing function)
esp_lcd_panel_handle_t panel_handle = NULL;
static bool usingSt7789 = false;
//Touch
static esp_lcd_touch_handle_t tp_handle = NULL; // Touch panel handle
static lv_indev_t *lvgl_touch_indev = NULL; // LVGL input device
/**
* @brief LVGL input device "read" callback.
* This function is called by LVGL periodically to get the current touch state.
*/
/*static void lvgl_touch_cb(lv_indev_t *indev, lv_indev_data_t *data)
{
// Get the touch handle stored in the input device's user_data
esp_lcd_touch_handle_t tp = (esp_lcd_touch_handle_t)lv_indev_get_user_data(indev);
// --- This is your code, now inside the callback ---
uint16_t touch_x[1];
uint16_t touch_y[1];
uint16_t touch_strength[1];
uint8_t touch_cnt = 0;
// Poll the touch controller
bool touchpad_pressed = esp_lcd_touch_get_coordinates(tp, touch_x, touch_y, touch_strength, &touch_cnt, 1);
if (touchpad_pressed && touch_cnt > 0) {
// A touch is detected
data->state = LV_INDEV_STATE_PRESSED;
data->point.x = touch_x[0];
data->point.y = touch_y[0];
} else {
// No touch is detected
data->state = LV_INDEV_STATE_RELEASED;
}
}*/
// --- Use the ESP32 pins from your code snippet ---
#define I2C_SDA_PIN 6
#define I2C_SCL_PIN 7
#define RST_N_PIN 4
#define INT_N_PIN 5
#define I2C_HOST I2C_NUM_0 // I2C port 0
// Global device handle
static ft6336u_dev_t touch_dev;
static i2c_master_bus_handle_t i2c_bus_handle;
// FreeRTOS queue to handle interrupt events
static QueueHandle_t touch_intr_queue = NULL;
/**
* @brief Initialize the I2C master bus
*/
esp_err_t i2c_master_init(void) {
i2c_master_bus_config_t i2c_bus_config = {
.i2c_port = I2C_HOST,
.sda_io_num = I2C_SDA_PIN,
.scl_io_num = I2C_SCL_PIN,
.clk_source = I2C_CLK_SRC_DEFAULT,
.glitch_ignore_cnt = 7,
.intr_priority = 0,
.flags.enable_internal_pullup = true
};
esp_err_t ret = i2c_new_master_bus(&i2c_bus_config, &i2c_bus_handle);
if (ret != ESP_OK) {
FreeOSLogE(TAG, "Failed to create I2C master bus: %s", esp_err_to_name(ret));
}
return ret;
}
/*void lvgl_touch_read_cb(lv_indev_t *indev, lv_indev_data_t *data) {
// --- LOCK ---
xSemaphoreTake(g_touch_mutex, QUEUE_MAX_DELAY);
// Read the shared variables
FreeOSLogI(TAG, "lvgl_touch_read_cb");
data->point.x = g_last_x;
data->point.y = g_last_y;
data->state = g_last_state;
// --- UNLOCK ---
xSemaphoreGive(g_touch_mutex);
}*/
/*void lvgl_touch_read_cb(lv_indev_t *indev, lv_indev_data_t *data) {
// Lock the mutex to check the state
xSemaphoreTake(g_touch_mutex, QUEUE_MAX_DELAY);
// --- This is the new logic ---
// 1. Check if a "press" event is waiting for us
if (g_last_state == LV_INDEV_STATE_PR) {
// It is. Report the press to LVGL.
data->point.x = g_last_x;
data->point.y = g_last_y;
data->state = LV_INDEV_STATE_PR;
// 2. IMPORTANT: Consume the event
// We immediately set the global state back to "released".
// This ensures LVGL only sees "PRESSED" for a single frame.
g_last_state = LV_INDEV_STATE_REL;
} else {
// No new press event. Just report "released".
data->state = LV_INDEV_STATE_REL;
}
// --- Unlock the mutex ---
xSemaphoreGive(g_touch_mutex);
}*/
/*void lvgl_touch_read_cb(lv_indev_t *indev, lv_indev_data_t *data) {
// --- LOCK THE MUTEX ---
xSemaphoreTake(g_touch_mutex, QUEUE_MAX_DELAY);
// --- THIS IS THE FIX ---
// 1. Check if a "press" event is waiting for us
if (g_last_state == LV_INDEV_STATE_PR) {
// It is. Report the press to LVGL.
data->point.x = g_last_x;
data->point.y = g_last_y;
data->state = LV_INDEV_STATE_PR;
// 2. IMPORTANT: Consume the event
// We immediately set the global state back to "released".
// This ensures LVGL only sees "PRESSED" for a single frame.
g_last_state = LV_INDEV_STATE_REL;
} else {
// No new press event. Just report "released".
data->point.x = g_last_x; // Pass last-known coords
data->point.y = g_last_y;
data->state = LV_INDEV_STATE_REL;
}
// --- UNLOCK THE MUTEX ---
xSemaphoreGive(g_touch_mutex);
}*/
void lvgl_touch_read_cb(lv_indev_t *indev, lv_indev_data_t *data) {
// Lock the mutex
xSemaphoreTake(g_touch_mutex, QUEUE_MAX_DELAY);
// --- This is the fix ---
// 1. Check if a "press" event is waiting for us
if (g_last_state == LV_INDEV_STATE_PR) {
// It is. Report the press to LVGL.
data->point.x = g_last_x;
data->point.y = g_last_y;
data->state = LV_INDEV_STATE_PR;
// 2. IMPORTANT: Consume the event
// We immediately set the global state back to "released".
g_last_state = LV_INDEV_STATE_REL;
} else {
// No new press event. Just report "released".
data->point.x = g_last_x;
data->point.y = g_last_y;
data->state = LV_INDEV_STATE_REL;
}
// Unlock the mutex
xSemaphoreGive(g_touch_mutex);
}
/*void lvgl_touch_read_cb(lv_indev_t *indev, lv_indev_data_t *data) {
FreeOSLogI(TAG, "lvgl_touch_read_cb");
FT6336U_TouchPointType touch_data;
// 1. Scan the touch controller
esp_err_t ret = ft6336u_scan(&touch_dev, &touch_data);
if (ret != ESP_OK) {
FreeOSLogE(TAG, "Failed to scan touch: %s", esp_err_to_name(ret));
// Keep last state if read fails
data->state = LV_INDEV_STATE_REL;
return;
}
// 2. Update LVGL with the new data
if (touch_data.touch_count == 0) {
data->state = LV_INDEV_STATE_REL; // Released
} else {
// Use the first touch point
data->point.x = touch_data.tp[0].x;
data->point.y = touch_data.tp[0].y;
data->state = LV_INDEV_STATE_PR; // Pressed
// Optional: Log if you still want to see coordinates
// FreeOSLogI(TAG, "Touch 0: X=%u, Y=%u", data->point.x, data->point.y);
}
}*/
/*void updateTouch(void *pvParameter) {
while (1) {
FT6336U_TouchPointType touch_data;
esp_err_t ret = ft6336u_scan(&touch_dev, &touch_data);
// --- LOCK ---
xSemaphoreTake(g_touch_mutex, QUEUE_MAX_DELAY);
if (ret != ESP_OK) {
g_last_state = LV_INDEV_STATE_REL;
} else {
if (touch_data.touch_count == 0) {
g_last_state = LV_INDEV_STATE_REL;
} else {
g_last_x = touch_data.tp[0].x;
g_last_y = touch_data.tp[0].y;
g_last_state = LV_INDEV_STATE_PR;
}
}
// --- UNLOCK ---
xSemaphoreGive(g_touch_mutex);
vTaskDelay(pdMS_TO_TICKS(20));
}
uint32_t event;
FT6336U_TouchPointType touch_data;
while (1) {
// Wait for an interrupt event from the queue
if (xQueueReceive(touch_intr_queue, &event, QUEUE_MAX_DELAY)) {
g_last_state = LV_INDEV_STATE_REL;
// Interrupt received, scan the touch controller
esp_err_t ret = ft6336u_scan(&touch_dev, &touch_data);
if (ret != ESP_OK) {
FreeOSLogE(TAG, "Failed to scan touch: %s", esp_err_to_name(ret));
g_last_state = LV_INDEV_STATE_REL;
continue;
}
if (touch_data.touch_count == 0) {
FreeOSLogI(TAG, "Touch Released");
g_last_state = LV_INDEV_STATE_REL;
} else {
FreeOSLogI(TAG, "Touch Pressed");
for (int i = 0; i < touch_data.touch_count; i++) {
FreeOSLogI(TAG, "Touch %d: (%s) X=%u, Y=%u",
i,
(touch_data.tp[i].status == touch) ? "NEW" : "STREAM",
touch_data.tp[i].x,
touch_data.tp[i].y);
}
g_last_x = touch_data.tp[0].x;
g_last_y = touch_data.tp[0].y;
g_last_state = LV_INDEV_STATE_PR;
}
}
}
}*/
#define MP3_READ_BUF_SIZE 16*1024
#define PCM_BUF_SIZE 1152 * 2
// Volume (0-256)
static int current_volume = 256; // 50%
// --- AUDIO CONFIGURATION ---
#define SAMPLE_RATE 44100
#define WAVE_FREQ_HZ 440.0f
#define PI 3.14159265f
static i2s_chan_handle_t tx_handle = NULL;
static i2s_chan_handle_t rx_handle = NULL;
// --- HELPER: 16-bit Volume Control ---
void write_to_i2s_16bit(i2s_chan_handle_t handle, int16_t *pcm_samples, int sample_count) {
size_t bytes_written;
// Apply volume in place
// We cast to int32_t temporarily to avoid overflow during multiplication
for (int i = 0; i < sample_count; i++) {
int32_t temp = (int32_t)pcm_samples[i] * current_volume;
pcm_samples[i] = (int16_t)(temp >> 8); // Divide by 256 and cast back
}
// Write DIRECTLY to I2S (No 32-bit conversion needed)
i2s_channel_write(handle, pcm_samples, sample_count * sizeof(int16_t), &bytes_written, QUEUE_MAX_DELAY);
}
void play_mp3_file(const char *filename, i2s_chan_handle_t tx_handle) {
FILE *f = fopen(filename, "rb");
if (!f) {
printf("Failed to open file: %s\n", filename);
return;
}
// --- CHANGE: Allocate in SPIRAM (PSRAM) ---
// This moves ~4KB + ~4KB out of internal RAM
uint8_t *read_buf = (uint8_t *)heap_caps_malloc(MP3_READ_BUF_SIZE, MALLOC_CAP_SPIRAM);
int16_t *pcm_buf = (int16_t *)heap_caps_malloc(PCM_BUF_SIZE * sizeof(int16_t), MALLOC_CAP_SPIRAM);
if (!read_buf || !pcm_buf) {
printf("Failed to allocate SPIRAM memory! Is PSRAM enabled in menuconfig?\n");
if (read_buf) free(read_buf);
if (pcm_buf) free(pcm_buf);
fclose(f);
return;
}
mp3dec_t mp3d;
mp3dec_frame_info_t info;
mp3dec_init(&mp3d);
int bytes_in_buf = 0;
int buf_offset = 0;
while (1) {
if (bytes_in_buf < MP3_READ_BUF_SIZE && !feof(f)) {
if (bytes_in_buf > 0) {
memmove(read_buf, read_buf + buf_offset, bytes_in_buf);
}
int bytes_read = fread(read_buf + bytes_in_buf, 1, MP3_READ_BUF_SIZE - bytes_in_buf, f);
bytes_in_buf += bytes_read;
buf_offset = 0;
if (bytes_read == 0 && bytes_in_buf == 0) break;
}
int samples = mp3dec_decode_frame(&mp3d, read_buf + buf_offset, bytes_in_buf, pcm_buf, &info);
buf_offset += info.frame_bytes;
bytes_in_buf -= info.frame_bytes;
if (samples > 0) {
// Send 16-bit data
write_to_i2s_16bit(tx_handle, pcm_buf, samples * info.channels);
} else {
if (bytes_in_buf > 0 && info.frame_bytes == 0) {
buf_offset++;
bytes_in_buf--;
}
}
}
free(read_buf);
free(pcm_buf);
fclose(f);
printf("Playback finished.\n");
}
void mp3_task_wrapper(void *arg) {
// We can pass the handle via valid logic, or just make tx_handle global
play_mp3_file("/sdcard/frair.mp3", tx_handle);
// Delete this task when song finishes so we don't crash
vTaskDelete(NULL);
}
typedef struct {
char riff_tag[4]; // "RIFF"
uint32_t riff_len; // Total file size - 8
char wave_tag[4]; // "WAVE"
char fmt_tag[4]; // "fmt "
uint32_t fmt_len; // 16 for PCM
uint16_t audio_format; // 1 for PCM
uint16_t num_channels; // 1 for Mono, 2 for Stereo
uint32_t sample_rate; // e.g., 44100
uint32_t byte_rate; // sample_rate * channels * (bit_depth/8)
uint16_t block_align; // channels * (bit_depth/8)
uint16_t bits_per_sample; // 16
char data_tag[4]; // "data"
uint32_t data_len; // Total bytes of audio data
} wav_header_t;
// Function to generate the header
void write_wav_header(FILE *f, int sample_rate, int channels, int total_data_len) {
wav_header_t header;
// RIFF Chunk
memcpy(header.riff_tag, "RIFF", 4);
header.riff_len = total_data_len + 36; // File size - 8
memcpy(header.wave_tag, "WAVE", 4);
// fmt Chunk
memcpy(header.fmt_tag, "fmt ", 4);
header.fmt_len = 16;
header.audio_format = 1; // PCM
header.num_channels = channels;
header.sample_rate = sample_rate;
header.bits_per_sample = 16;
header.byte_rate = sample_rate * channels * (16 / 8);
header.block_align = channels * (16 / 8);
// data Chunk
memcpy(header.data_tag, "data", 4);
header.data_len = total_data_len;
// Write header to beginning of file
fseek(f, 0, SEEK_SET); // Jump to start
fwrite(&header, sizeof(wav_header_t), 1, f);
fseek(f, 0, SEEK_END); // Jump back to end
}
#define RECORD_TIME_SEC 10
#define SAMPLE_RATE 44100
#define CHANNELS 2 // We use Stereo mode in I2S, even if mic is mono
#define BIT_DEPTH 16
// Control Flags
static volatile bool is_recording_active = false;
static TaskHandle_t record_task_handle = NULL;
// Make sure your I2S RX Handle is global so the task can see it
// (Define this near your setupHybridAudio function)
extern i2s_chan_handle_t rx_handle;
void record_wav_task(void *args) {
FreeOSLogI("REC", "Opening file for recording...");
// 1. Open File
FILE *f = fopen("/sdcard/recording.wav", "wb");
if (f == NULL) {
FreeOSLogE("REC", "Failed to open file!");
is_recording_active = false;
record_task_handle = NULL;
vTaskDelete(NULL);
return;
}
// 2. Write Placeholder Header
wav_header_t dummy_header = {0};
fwrite(&dummy_header, sizeof(wav_header_t), 1, f);
// 3. Allocate Buffer
size_t chunk_size = 4096;
char *buffer = cc_safe_alloc(1, chunk_size);
size_t bytes_read = 0;
size_t total_bytes_recorded = 0;
// 4. The Loop (Runs while is_recording_active is TRUE)
while (is_recording_active) {
// Read audio from I2S
if (i2s_channel_read(rx_handle, buffer, chunk_size, &bytes_read, 100) == ESP_OK) {
// Write audio to SD Card
if (bytes_read > 0) {
fwrite(buffer, 1, bytes_read, f);
total_bytes_recorded += bytes_read;
}
} else {
// Optional: Short delay to prevent watchdog starvation if I2S fails
vTaskDelay(pdMS_TO_TICKS(10));
}
}
// 5. Cleanup (The "Graceful" part)
FreeOSLogI("REC", "Stopping... Finalizing WAV Header.");
// Update the header with the correct file size
write_wav_header(f, 44100, 2, total_bytes_recorded);
fclose(f);
free(buffer);
FreeOSLogI("REC", "File saved. Task deleting.");
record_task_handle = NULL; // Reset handle
vTaskDelete(NULL);
}
void start_recording(void) {
if (is_recording_active) {
ESP_LOGW("REC", "Already recording!");
return;
}
if (rx_handle == NULL) {
FreeOSLogE("REC", "I2S RX Handle is NULL! Did you run setupHybridAudio?");
return;
}
is_recording_active = true;
FreeOSLogI("REC", "rec_task");
// Create the task (Pinned to Core 1 is usually best for file I/O)
xTaskCreatePinnedToCore(record_wav_task, "rec_task", 1024 * 6, NULL, 5, &record_task_handle, 1);
}
void stop_recording(void) {
if (!is_recording_active) {
return;
}
FreeOSLogI("REC", "Stop requested...");
// Flip the switch. The task will exit its while() loop and clean up.
is_recording_active = false;
}
void record_task(void *args) {
// 1. Open File
FILE *f = fopen("/sdcard/recording.wav", "wb");
if (f == NULL) {
FreeOSLogE("REC", "Failed to open file for writing");
vTaskDelete(NULL);
}
// 2. Write Dummy Header (We will fix the sizes later)
wav_header_t dummy_header = {0};
fwrite(&dummy_header, sizeof(wav_header_t), 1, f);
// 3. Allocation
size_t bytes_read = 0;
// Buffer size: 1024 samples * 2 channels * 2 bytes = 4096 bytes
size_t chunk_size = 4096;
char *buffer = cc_safe_alloc(1, chunk_size);
int total_bytes_recorded = 0;
int expected_bytes = RECORD_TIME_SEC * SAMPLE_RATE * CHANNELS * (BIT_DEPTH/8);
FreeOSLogI("REC", "Start Recording...");
// 4. Recording Loop
while (total_bytes_recorded < expected_bytes) {
// Read from Microphone (I2S RX Handle)
// Note: Use 'rx_handle' here, NOT tx_handle!
if (i2s_channel_read(rx_handle, buffer, chunk_size, &bytes_read, 1000) == ESP_OK) {
// Write Raw Data to SD Card
fwrite(buffer, 1, bytes_read, f);
total_bytes_recorded += bytes_read;
}
}
FreeOSLogI("REC", "Recording Complete. Finalizing...");
// 5. Finalize Header
// Go back to the top of the file and write the correct lengths
write_wav_header(f, SAMPLE_RATE, CHANNELS, total_bytes_recorded);
// 6. Cleanup
fclose(f);
free(buffer);
vTaskDelete(NULL);
}
void turn_off_wifi_and_free_ram(void) {
ESP_LOGW("WIFI", "Shutting down Wi-Fi to reclaim RAM...");
// 1. Stop the Wi-Fi Driver (Turns off Radio, saves power)
// Memory is STILL allocated here.
esp_err_t err = esp_wifi_stop();
if (err == ESP_ERR_WIFI_NOT_INIT) {
ESP_LOGW("WIFI", "Wi-Fi was not initialized, nothing to do.");
return;
}
ESP_ERROR_CHECK(err);
// 2. De-Initialize the Driver (The Magic Step)
// This effectively "un-mallocs" the 60KB+ of Internal RAM.
ESP_ERROR_CHECK(esp_wifi_deinit());
// Optional: Destroy the default pointers if you created them
// (This cleans up small remaining handles, but deinit does the heavy lifting)
// esp_netif_destroy_default_wifi(your_netif_handle);
ESP_LOGW("WIFI", "Wi-Fi De-initialized. RAM should be back.");
// Prove it
FreeOSLogI("MEM", "Free Internal Heap: %ld bytes", (long)heap_caps_get_free_size(MALLOC_CAP_INTERNAL));
}
// 1. One-Time Initialization
// Call this inside app_main() BEFORE you show any UI!
void init_wifi_stack_once(void) {
checkAvailableMemory();
if (wifi_initialized) return; // Prevention
// Initialize NVS (Required for WiFi)
/* esp_err_t ret = nvs_flash_init();
if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) {
ESP_ERROR_CHECK(nvs_flash_erase());
ret = nvs_flash_init();
}
ESP_ERROR_CHECK(ret);*/
// Initialize Network Stack
ESP_ERROR_CHECK(esp_netif_init());
ESP_ERROR_CHECK(esp_event_loop_create_default());
esp_netif_t *sta_netif = esp_netif_create_default_wifi_sta();
assert(sta_netif);
wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
// 1. Slash the RX/TX Buffers (The biggest RAM savers)
// Default is usually 32. We drop to 4-8.
// Note: This might reduce max throughput speed, but fixes the crash.
cfg.dynamic_rx_buf_num = 4; // Minimum safe value
cfg.cache_tx_buf_num = 4; // Minimum safe value
// 2. Cripple the AMPDU (Aggregation)
// AMPDU is "High Speed Mode". We don't need 100mbps for audio.
cfg.ampdu_rx_enable = 0; // Disable completely if possible, or set extremely low
cfg.ampdu_tx_enable = 0; // Disable completely
// 3. Force "nvs" off (Optional, saves a tiny bit of RAM/Flash ops)
cfg.nvs_enable = 0;
ESP_ERROR_CHECK(esp_wifi_init(&cfg));
ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA));
ESP_ERROR_CHECK(esp_wifi_start());
esp_wifi_set_ps(WIFI_PS_NONE);
wifi_initialized = true;
printf("WiFi Stack Initialized.\n");
checkAvailableMemory();
}
void trigger_wifi_scan(void) {
if (!wifi_initialized) {
printf("Error: WiFi not initialized! Call init_wifi_stack_once() first.\n");
return;
}
// Reset to first page
g_wifi_page_index = 0;
printf("Starting WiFi Scan...\n");
// 1. Start Scan (Blocking)
// This blocks for ~1.5s. Ensure Watchdog doesn't bite if this task is high priority.
esp_wifi_scan_start(NULL, true);
// 2. Get number of results found
uint16_t ap_count = 0;
ESP_ERROR_CHECK(esp_wifi_scan_get_ap_num(&ap_count));
// Safety limit
if (ap_count > MAX_WIFI_RESULTS) ap_count = MAX_WIFI_RESULTS;
// 3. FIX: Allocate buffer on HEAP, not Stack
wifi_ap_record_t *ap_info = (wifi_ap_record_t *)calloc(ap_count, sizeof(wifi_ap_record_t));
if (!ap_info) {
printf("CRITICAL: Out of memory for WiFi scan!\n");
return;
}
// 4. Get Records into Heap Buffer
ESP_ERROR_CHECK(esp_wifi_scan_get_ap_records(&ap_count, ap_info));
// 5. Update Global Data
g_wifi_scan_count = ap_count;
for (int i = 0; i < ap_count; i++) {
// Safe Copy
strncpy(g_wifi_scan_results[i].ssid, (char *)ap_info[i].ssid, 32);
g_wifi_scan_results[i].ssid[32] = '\0';
g_wifi_scan_results[i].rssi = ap_info[i].rssi;
g_wifi_scan_results[i].channel = ap_info[i].primary;
g_wifi_scan_results[i].auth_mode = ap_info[i].authmode;
// Debug
// printf("[%d] %s (%d)\n", i, g_wifi_scan_results[i].ssid, g_wifi_scan_results[i].rssi);
}
// 6. Free the Heap Buffer
free(ap_info);
printf("Scan Done. Found %d networks.\n", g_wifi_scan_count);
// 7. Refresh UI
refresh_wifi_list_ui();
update_full_ui();
}
// Context struct to pass data into the decoder callback
typedef struct {
ImageTexture *texture; // Your texture struct
int outputX; // Where to draw it on the texture
int outputY;
} JpegDev;
static UINT tjd_input(JDEC* jd, BYTE* buff, UINT nd) {
FILE *f = (FILE *)jd->device;
if (buff) {
size_t read = fread(buff, 1, nd, f);
// If we hit EOF, log it but return what we got
if (read < nd && feof(f)) {
// ESP_LOGW("MJPEG", "Input: EOF hit unexpectedly"); // Comment out if spammy
}
return (UINT)read;
} else {
if (fseek(f, nd, SEEK_CUR) == 0) return nd;
return 0;
}
}
// 2. OUTPUT FUNC: Receives decoded RGB888 blocks and converts to RGBA
static UINT tjd_output(JDEC* jd, void* bitmap, JRECT* rect) {
JpegDev *dev = (JpegDev *)jd->device;
ImageTexture *tex = dev->texture;
// 'bitmap' is an array of RGB bytes (R, G, B, R, G, B...)
uint8_t *rgb = (uint8_t *)bitmap;
// Loop through the rectangular block provided by the decoder
for (int y = rect->top; y <= rect->bottom; y++) {
for (int x = rect->left; x <= rect->right; x++) {
// Safety check: Don't write outside your texture memory
if (x < tex->width && y < tex->height) {
// Calculate target index in your RGBA array
int index = y * tex->width + x;
// Convert RGB (3 bytes) to RGBA (4 bytes)
tex->data[index].r = rgb[0];
tex->data[index].g = rgb[1];
tex->data[index].b = rgb[2];
tex->data[index].a = 255; // Fully opaque
}
// Move source pointer forward 3 bytes (R,G,B)
rgb += 3;
}
}
return 1; // Continue decoding
}
#define READ_CHUNK_SIZE 4096
// Update to match your screen resolution
#define VID_W 480
#define VID_H 320
void video_player_task(void *pvParam) {
// 1. Correct Path
const char *filename = "/sdcard/output.mjpeg";
FILE *f = fopen(filename, "rb");
if (!f) {
FreeOSLogE(TAG, "Failed to open %s", filename);
vTaskDelete(NULL);
}
// 2. Get File Size for EOF checks
fseek(f, 0, SEEK_END);
long fileSize = ftell(f);
fseek(f, 0, SEEK_SET);
FreeOSLogI(TAG, "File Size: %ld", fileSize);
// 3. Allocate 480x320 Buffers
ImageTexture *frameBuffer = heap_caps_malloc(sizeof(ImageTexture), MALLOC_CAP_SPIRAM);
frameBuffer->width = VID_W;
frameBuffer->height = VID_H;
// 480x320x4 bytes = ~600KB. Ensure your ESP32 has PSRAM enabled.
frameBuffer->data = heap_caps_malloc(VID_W * VID_H * sizeof(ColorRGBA), MALLOC_CAP_SPIRAM);
if (!frameBuffer->data) {
FreeOSLogE(TAG, "Failed to allocate video memory! Check PSRAM.");
fclose(f);
vTaskDelete(NULL);
}
char *work = heap_caps_malloc(4096, MALLOC_CAP_INTERNAL);
uint8_t *chunkBuf = heap_caps_malloc(READ_CHUNK_SIZE, MALLOC_CAP_INTERNAL);
JDEC jdec;
JpegDev dev;
dev.texture = frameBuffer;
FreeOSLogI(TAG, "Starting 480x320 Video...");
while (1) {
bool foundFrame = false;
long frameStartOffset = 0;
// --- SCANNING ---
while (!foundFrame) {
long currentPos = ftell(f);
size_t bytesRead = fread(chunkBuf, 1, READ_CHUNK_SIZE, f);
// EOF Check
if (bytesRead < 2) {
clearerr(f); fseek(f, 0, SEEK_SET); continue;
}
for (int i = 0; i < bytesRead - 1; i++) {
if (chunkBuf[i] == 0xFF && chunkBuf[i+1] == 0xD8) {
foundFrame = true;
frameStartOffset = currentPos + i;
break;
}
}
if (!foundFrame) vTaskDelay(1);
}
// --- DECODE ---
// Safety: If we found a frame very close to the end (e.g. last 1KB), it's likely truncated.
if (fileSize - frameStartOffset < 1024) {
ESP_LOGW(TAG, "Skipping truncated frame at end of file.");
clearerr(f);
fseek(f, 0, SEEK_SET);
continue;
}
clearerr(f);
fseek(f, frameStartOffset, SEEK_SET);
JRESULT res = jd_prepare(&jdec, tjd_input, work, 4096, f);
if (res == JDR_OK) {
jdec.device = &dev;
res = jd_decomp(&jdec, tjd_output, 0);
if (res == JDR_OK) {
// SUCCESS
//FreeOSLogI("GFX", "Sent Pixel Buffer Command");
GraphicsCommand cmd = {
.cmd = CMD_DRAW_PIXEL_BUFFER,
.x = 0, .y = 0,
.w = VID_W, .h = VID_H, // 480x320
.pixelBuffer = frameBuffer->data
};
QueueSend(g_graphics_queue, &cmd, QUEUE_MAX_DELAY);
GraphicsCommand cmd_flush = {
.cmd = CMD_UPDATE_AREA,
.x = 0, .y = 0, .w = VID_W, .h = VID_H
};
QueueSend(g_graphics_queue, &cmd_flush, QUEUE_MAX_DELAY);
}
else {
//FreeOSLogI("GFX", "Decode Failed");
// Decode Failed (Likely EOF hit unexpectedly)
// If we fail, assume this frame is bad/truncated.
// If we are near the end of the file (>90%), just restart.
if (frameStartOffset > fileSize * 0.9) {
ESP_LOGW(TAG, "EOF hit near end. Restarting video.");
fseek(f, 0, SEEK_SET);
continue;
}
}
}
else {
// Prepare failed.
if (frameStartOffset > fileSize * 0.9) {
fseek(f, 0, SEEK_SET);
continue;
}
}
// Advance Ptr
clearerr(f);
fseek(f, frameStartOffset + 2, SEEK_SET);
// Frame Pacing (480x320 takes longer to transfer, so 30ms might be optimistic)
// If it feels slow, decrease this to 10 or 0.
vTaskDelay(pdMS_TO_TICKS(10));
}
}
typedef struct {
FILE* f;
CCImage* img;
} ThumbContext;
static UINT tjd_input_thumb(JDEC* jd, BYTE* buff, UINT nd) {
ThumbContext *ctx = (ThumbContext *)jd->device;
if (buff) return (UINT)fread(buff, 1, nd, ctx->f);
return fseek(ctx->f, nd, SEEK_CUR) == 0 ? nd : 0;
}
static UINT tjd_output_thumb(JDEC* jd, void* bitmap, JRECT* rect) {
ThumbContext *ctx = (ThumbContext *)jd->device;
CCImage *img = ctx->img; // Get the image from context
uint8_t *rgb = (uint8_t *)bitmap;
for (int y = rect->top; y <= rect->bottom; y++) {
for (int x = rect->left; x <= rect->right; x++) {
if (x < img->size->width && y < img->size->height) {
int index = y * img->size->width + x;
ColorRGBA *pixel = &((ColorRGBA*)img->imageData)[index];
pixel->r = rgb[0];
pixel->g = rgb[1];
pixel->b = rgb[2];
pixel->a = 255;
}
rgb += 3;
}
}
return 1;
}
static UINT tjd_input_one_frame(JDEC* jd, BYTE* buff, UINT nd) {
FILE *f = (FILE *)jd->device;
if (buff) return (UINT)fread(buff, 1, nd, f);
return fseek(f, nd, SEEK_CUR) == 0 ? nd : 0;
}
// Output: Writes RGBA to CCImage buffer
static UINT tjd_output_one_frame(JDEC* jd, void* bitmap, JRECT* rect) {
// We pass the CCImage* as the device context
CCImage *img = (CCImage *)jd->device;
uint8_t *rgb = (uint8_t *)bitmap;
for (int y = rect->top; y <= rect->bottom; y++) {
for (int x = rect->left; x <= rect->right; x++) {
if (x < img->size->width && y < img->size->height) {
int index = y * img->size->width + x;
ColorRGBA *pixel = &((ColorRGBA*)img->imageData)[index];
pixel->r = rgb[0];
pixel->g = rgb[1];
pixel->b = rgb[2];
pixel->a = 255; // Opaque
}
rgb += 3;
}
}
return 1;
}
// Corrected Main Function Call
CCImage* imageWithVideoFrame(const char *path, int width, int height) {
FILE *f = fopen(path, "rb");
if (!f) {
FreeOSLogE("IMG", "Could not open video: %s", path);
return NULL;
}
FreeOSLogI("IMG", "Could open video: %s", path);
// 1. Allocate CCImage in PSRAM
CCImage *img = image();
img->size->width = width;
img->size->height = height;
img->imageData = heap_caps_malloc(width * height * sizeof(ColorRGBA), MALLOC_CAP_SPIRAM);
// 2. Scan for First Frame (FF D8)
// We scan the first 4KB. If the frame isn't there, it's not a valid MJPEG.
uint8_t *chunk = cc_safe_alloc(1, 4096);
size_t read = fread(chunk, 1, 4096, f);
long headerOffset = -1;
for (int i = 0; i < read - 1; i++) {
if (chunk[i] == 0xFF && chunk[i+1] == 0xD8) {
headerOffset = i;
break;
}
}
free(chunk);
if (headerOffset == -1) {
FreeOSLogE("IMG", "No frame found in video header");
fclose(f);
// Return a blank/error image or NULL
return img;
}
// 3. Decode
fseek(f, headerOffset, SEEK_SET); // Jump to the Start of Image
char *work = cc_safe_alloc(1, 4096);
JDEC jdec;
// Context holds both the File and the Target Image
ThumbContext ctx;
ctx.f = f;
ctx.img = img;
// Use specific helpers for this context
if (jd_prepare(&jdec, tjd_input_thumb, work, 4096, &ctx) == JDR_OK) {
jd_decomp(&jdec, tjd_output_thumb, 0);
}
// --- DEBUG: LOG PIXEL DATA (FIXED) ---
FreeOSLogI("IMG", "--- PIXEL DATA DUMP ---");
// 1. Cast the raw bytes to a Pixel Array
ColorRGBA *pixels = (ColorRGBA *)img->imageData;
// Print First 5 Pixels (Top Left)
for (int i = 0; i < 5; i++) {
ColorRGBA p = pixels[i]; // Now this works
FreeOSLogI("IMG", "Pixel[%d]: R=%d G=%d B=%d A=%d", i, p.r, p.g, p.b, p.a);
}
// Print Center Pixel
int centerIdx = (height/2) * width + (width/2);
ColorRGBA p = pixels[centerIdx];
FreeOSLogI("IMG", "Pixel[Center]: R=%d G=%d B=%d A=%d", p.r, p.g, p.b, p.a);
free(work);
fclose(f);
return img;
}
typedef struct {
FILE *f;
CCImageView *view;
uint8_t *chunkBuf; // Buffer for scanning
char *workBuf; // Buffer for decoder
CCImage *imgBuffer; // Reusable image memory
} VideoState;
void next_frame_task(void *pvParam) {
int64_t t3 = esp_timer_get_time();
CCImageView *targetView = (CCImageView *)pvParam;
const char *path = "/sdcard/output.mjpeg";
// 1. Setup State
FILE *f = fopen(path, "rb");
if (!f) {
FreeOSLogE("VID", "Cannot open %s", path);
vTaskDelete(NULL);
}
// Allocate reusable buffers (Don't malloc inside the loop!)
uint8_t *chunkBuf = heap_caps_malloc(4096, MALLOC_CAP_INTERNAL);
char *workBuf = heap_caps_malloc(4096, MALLOC_CAP_INTERNAL);
// Create the image buffer ONCE. We will just overwrite its pixels.
CCImage *img = image();
img->size->width = 320;
img->size->height = 480;
img->imageData = heap_caps_malloc(480 * 320 * sizeof(ColorRGBA), MALLOC_CAP_SPIRAM);
// Assign this image to the view immediately
targetView->image = img;
JDEC jdec;
ThumbContext ctx; // Reuse our context helper from before
ctx.f = f;
ctx.img = img;
FreeOSLogI("VID", "Starting Animation Loop...");
while (1) {
bool foundFrame = false;
long frameStart = 0;
// --- 2. Scan for Next Frame ---
while (!foundFrame) {
long pos = ftell(f);
size_t read = fread(chunkBuf, 1, 4096, f);
// Loop video at EOF
if (read < 2) {
clearerr(f); fseek(f, 0, SEEK_SET); continue;
}
for (int i = 0; i < read - 1; i++) {
if (chunkBuf[i] == 0xFF && chunkBuf[i+1] == 0xD8) {
foundFrame = true;
frameStart = pos + i;
break;
}
}
}
// --- 3. Decode Frame ---
clearerr(f);
fseek(f, frameStart, SEEK_SET);
if (jd_prepare(&jdec, tjd_input_thumb, workBuf, 4096, &ctx) == JDR_OK) {
if (jd_decomp(&jdec, tjd_output_thumb, 0) == JDR_OK) {
// --- 4. Trigger Redraw ---
// We don't send pixel data here. We tell the Graphics Task
// "The view has changed, please redraw the tree."
// Note: The graphics task reads targetView->image->imageData,
// which we just overwrote with new pixels.
GraphicsCommand cmd = {
.cmd = CMD_DRAW_PIXEL_BUFFER, // Or your custom view redraw command
.x = 0, .y = 0, .w = 320, .h = 480,
.pixelBuffer = img->imageData
};
QueueSend(g_graphics_queue, &cmd, 0);
//update_view_only(targetView);
GraphicsCommand cmd_flush = {
.cmd = CMD_UPDATE_AREA,
.x = 0, .y = 0, .w = 320, .h = 480
};
QueueSend(g_graphics_queue, &cmd_flush, QUEUE_MAX_DELAY);
}
}
// --- 5. Advance & Wait ---
clearerr(f);
fseek(f, frameStart + 2, SEEK_SET);
// 0.1s Delay (100ms)
//vTaskDelay(pdMS_TO_TICKS(25));
int64_t t4 = esp_timer_get_time();
FreeOSLogI(TAG, "nxt frame Time: %lld us", (t4 - t3));
}
}
int currentFrame = 0;
void next_frame_task1(void *pvParam) {
while (1) {
CCImageView *targetView = (CCImageView *)pvParam;
CCString* path = stringWithFormat("/sdcard/frames/img_%d.png", currentFrame);
targetView->image = imageWithFile(path);
/*GraphicsCommand cmd = {
.cmd = CMD_DRAW_PIXEL_BUFFER, // Or your custom view redraw command
.x = 0, .y = 0, .w = 320, .h = 480,
.pixelBuffer = targetView->image->imageData
};
QueueSend(g_graphics_queue, &cmd, 0);*/
//update_view_only(targetView);
GraphicsCommand cmd_flush = {
.cmd = CMD_UPDATE_AREA,
.x = 0, .y = 0, .w = 320, .h = 480
};
QueueSend(g_graphics_queue, &cmd_flush, QUEUE_MAX_DELAY);
currentFrame++;
// 0.1s Delay (100ms)
vTaskDelay(pdMS_TO_TICKS(50));
}
}
CCImageView* videoView = NULL;
void setup_video_preview_ui(void) {
// 1. Create the Image View
videoView = imageViewWithFrame(ccRect(0, 0, 320, 480));
// 2. Load JUST the first frame (480x320 resolution)
// Note: This might take ~200-500ms, blocking the UI briefly.
videoView->image = imageWithVideoFrame("/sdcard/output.mjpeg", 320, 480);
FreeOSLogI("IMG", "imageWithVideoFrame");
// 3. Add to screen
viewAddSubview(mainWindowView, videoView);
}
void setup_video_preview_ui1(void) {
// 1. Create the Image View
videoView = imageViewWithFrame(ccRect(0, 0, 320, 480));
// 2. Load JUST the first frame (480x320 resolution)
// Note: This might take ~200-500ms, blocking the UI briefly.
videoView->image = imageWithFile(ccs("/sdcard/frames/img_1.png"));
FreeOSLogI("IMG", "imageWithVideoFrame");
// 3. Add to screen
viewAddSubview(mainWindowView, videoView);
}
// CONSTANTS FROM FFMPEG SETUP
#define VIDEO_WIDTH 320 // Transposed width
#define VIDEO_HEIGHT 480 // Transposed height
// ... (Keep your Framebuffer struct and yuv2rgb helpers here) ...
typedef struct {
Framebuffer *fb;
const char *filepath;
} VideoTaskParams;
void vVideoDecodeTask(void *pvParameters) {
VideoTaskParams *params = (VideoTaskParams *)pvParameters;
Framebuffer *fb = params->fb;
const char *path = params->filepath;
FreeOSLogI(TAG, "Opening video file: %s", path);
FILE *f = fopen(path, "rb");
if (f == NULL) {
FreeOSLogE(TAG, "Failed to open file");
vTaskDelete(NULL);
return;
}
// 1. INITIAL SETUP
esp_h264_dec_cfg_sw_t cfg = { .pic_type = ESP_H264_RAW_FMT_I420 };
esp_h264_dec_handle_t dec_handle = NULL;
esp_h264_dec_sw_new(&cfg, &dec_handle);
esp_h264_dec_open(dec_handle);
// 2. ALLOCATE BUFFER
const int BUFFER_CAPACITY = 1024 * 512;
// Verify we have enough PSRAM
uint8_t *file_buffer = (uint8_t *)heap_caps_malloc(BUFFER_CAPACITY, MALLOC_CAP_SPIRAM);
if (file_buffer == NULL) {
FreeOSLogE(TAG, "Failed to allocate 512KB buffer! Trying 256KB...");
// Fallback if your specific board is low on RAM
file_buffer = (uint8_t *)heap_caps_malloc(1024 * 256, MALLOC_CAP_SPIRAM);
}
size_t current_data_len = 0;
esp_h264_dec_in_frame_t in_frame = { 0 };
esp_h264_dec_out_frame_t out_frame = { 0 };
FreeOSLogI(TAG, "Starting decode loop...");
while (1) {
// A. READ DATA
size_t space_available = BUFFER_CAPACITY - current_data_len;
if (space_available < 1024) {
current_data_len = 0;
space_available = BUFFER_CAPACITY;
}
size_t bytes_read = fread(file_buffer + current_data_len, 1, space_available, f);
if (bytes_read == 0) {
fseek(f, 0, SEEK_SET);
current_data_len = 0;
vTaskDelay(pdMS_TO_TICKS(10));
continue;
}
current_data_len += bytes_read;
// B. DECODE LOOP
in_frame.raw_data.buffer = file_buffer;
in_frame.raw_data.len = current_data_len;
in_frame.dts = 0;
in_frame.pts = 0;
while (in_frame.raw_data.len > 0) {
esp_h264_err_t ret = esp_h264_dec_process(dec_handle, &in_frame, &out_frame);
// --- CASE 1: SUCCESS ---
if (ret == ESP_H264_ERR_OK) {
if (out_frame.outbuf != NULL) {
// Draw Frame (Optimized YUV->RGB)
int width = VIDEO_WIDTH;
int height = VIDEO_HEIGHT;
uint8_t *y_plane = out_frame.outbuf;
uint8_t *u_plane = y_plane + (width * height);
uint8_t *v_plane = u_plane + (width * height / 4);
int u_stride = width / 2;
for (int y = 0; y < height; y += 2) {
for (int x = 0; x < width; x += 2) {
if (y >= fb->displayHeight || x >= fb->displayWidth) continue;
int uv_off = (y>>1)*u_stride + (x>>1);
int d = u_plane[uv_off] - 128;
int e = v_plane[uv_off] - 128;
int rc = (409*e+128);
int gc = (-100*d-208*e+128);
int bc = (516*d+128);
int y_idx[4] = { y*width+x, y*width+x+1, (y+1)*width+x, (y+1)*width+x+1 };
int fb_idx[4] = {
(y*fb->displayWidth+x)*3, (y*fb->displayWidth+x+1)*3,
((y+1)*fb->displayWidth+x)*3, ((y+1)*fb->displayWidth+x+1)*3
};
for(int k=0; k<4; k++) {
int py = y+(k/2), px = x+(k%2);
if(py>=fb->displayHeight || px>=fb->displayWidth) continue;
int y_term = 298*(y_plane[y_idx[k]]-16);
uint8_t *p = (uint8_t*)fb->pixelData + fb_idx[k];
int r = (y_term+rc)>>8; p[0] = (r>255)?255:(r<0?0:r);
int g = (y_term+gc)>>8; p[1] = (g>255)?255:(g<0?0:g);
int b = (y_term+bc)>>8; p[2] = (b>255)?255:(b<0?0:b);
}
}
}
}
if (in_frame.consume > 0) {
in_frame.raw_data.buffer += in_frame.consume;
in_frame.raw_data.len -= in_frame.consume;
} else {
break; // Need more data
}
}
// --- CASE 2: FATAL ERROR (RESTART VIDEO) ---
else {
FreeOSLogE(TAG, "Decoder Error %d. RESTARTING VIDEO to restore sync...", ret);
// 1. Destroy Corrupted Decoder
esp_h264_dec_close(dec_handle);
esp_h264_dec_del(dec_handle);
// 2. REWIND FILE (The Fix)
// We must go back to start to read SPS/PPS headers again
fseek(f, 0, SEEK_SET);
// 3. Clear Buffer
current_data_len = 0;
in_frame.raw_data.len = 0; // Forces break from inner loop
// 4. Create Fresh Decoder
dec_handle = NULL;
esp_h264_dec_sw_new(&cfg, &dec_handle);
esp_h264_dec_open(dec_handle);
ESP_LOGW(TAG, "Video Restarted.");
break; // Break inner loop to trigger fread from byte 0
}
}
// C. PRESERVE LEFTOVERS
size_t leftovers = in_frame.raw_data.len;
if (leftovers > 0) {
memmove(file_buffer, in_frame.raw_data.buffer, leftovers);
}
current_data_len = leftovers;
vTaskDelay(pdMS_TO_TICKS(10));
}
esp_h264_dec_close(dec_handle);
esp_h264_dec_del(dec_handle);
free(file_buffer);
fclose(f);
vTaskDelete(NULL);
}
void start_video_player(Framebuffer *fb) {
// You must allocate this structure so it persists after this function returns
VideoTaskParams *params = cc_safe_alloc(1, sizeof(VideoTaskParams));
params->fb = fb;
// Assuming you have mounted SD card to /sdcard
params->filepath = "/sdcard/output.h264";
xTaskCreatePinnedToCore(vVideoDecodeTask, "VidTask", 16384, (void*)params, 2, NULL, 1);
}
// --- HELPER: YUV420 to RGB888 ---
// Returns a new buffer that you must free() later
uint8_t* yuv420_to_rgb888(uint8_t *y_plane, uint8_t *u_plane, uint8_t *v_plane, int width, int height) {
// Allocate RGB Buffer (Width * Height * 3 bytes)
uint8_t *rgb_buffer = (uint8_t *)heap_caps_malloc(width * height * 3, MALLOC_CAP_SPIRAM);
if (!rgb_buffer) return NULL;
int u_stride = width / 2;
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
// Get Y (Luma)
int Y_val = y_plane[y * width + x];
// Get UV (Chroma) - Shared by 2x2 pixels
int uv_idx = (y >> 1) * u_stride + (x >> 1);
int U_val = u_plane[uv_idx];
int V_val = v_plane[uv_idx];
// Math
int c = Y_val - 16;
int d = U_val - 128;
int e = V_val - 128;
int r = (298 * c + 409 * e + 128) >> 8;
int g = (298 * c - 100 * d - 208 * e + 128) >> 8;
int b = (298 * c + 516 * d + 128) >> 8;
// Clamp
if (r > 255) r = 255; else if (r < 0) r = 0;
if (g > 255) g = 255; else if (g < 0) g = 0;
if (b > 255) b = 255; else if (b < 0) b = 0;
// Write to buffer
int idx = (y * width + x) * 3;
rgb_buffer[idx] = (uint8_t)r;
rgb_buffer[idx+1] = (uint8_t)g;
rgb_buffer[idx+2] = (uint8_t)b;
}
}
return rgb_buffer;
}
// --- MAIN FUNCTION: GET FIRST FRAME ---
// Returns: A pointer to RGB888 data (in PSRAM). Returns NULL on failure.
// Note: You must free() the result when done!
uint8_t* get_first_video_frame(const char *filepath, int width, int height) {
FreeOSLogI("VID", "Extracting first frame from %s...", filepath);
FILE *f = fopen(filepath, "rb");
if (f == NULL) {
FreeOSLogE("VID", "File not found");
return NULL;
}
// 1. Setup Decoder
esp_h264_dec_cfg_sw_t cfg = { .pic_type = ESP_H264_RAW_FMT_I420 };
esp_h264_dec_handle_t dec_handle = NULL;
if (esp_h264_dec_sw_new(&cfg, &dec_handle) != ESP_H264_ERR_OK) {
fclose(f);
return NULL;
}
esp_h264_dec_open(dec_handle);
// 2. Allocate Temp Buffer (Large enough for one I-Frame)
const int CHUNK_SIZE = 1024 * 64;
uint8_t *file_buffer = (uint8_t *)heap_caps_malloc(CHUNK_SIZE, MALLOC_CAP_SPIRAM);
uint8_t *result_pixels = NULL;
int decoded = 0;
esp_h264_dec_in_frame_t in_frame = { 0 };
esp_h264_dec_out_frame_t out_frame = { 0 };
// 3. Read Loop (Only runs until first frame is found)
while (!decoded) {
size_t bytes_read = fread(file_buffer, 1, CHUNK_SIZE, f);
if (bytes_read == 0) break; // EOF
in_frame.raw_data.buffer = file_buffer;
in_frame.raw_data.len = bytes_read;
while (in_frame.raw_data.len > 0) {
esp_h264_err_t ret = esp_h264_dec_process(dec_handle, &in_frame, &out_frame);
if (ret == ESP_H264_ERR_OK && out_frame.outbuf != NULL) {
FreeOSLogI("VID", "Frame found! Converting...");
// Pointers for YUV420 Planar
uint8_t *y = out_frame.outbuf;
uint8_t *u = y + (width * height);
uint8_t *v = u + (width * height / 4);
// Convert to RGB
result_pixels = yuv420_to_rgb888(y, u, v, width, height);
decoded = 1; // STOP LOOP
break;
}
if (in_frame.consume > 0) {
in_frame.raw_data.buffer += in_frame.consume;
in_frame.raw_data.len -= in_frame.consume;
} else {
break; // Fetch more data
}
}
}
// 4. Cleanup
free(file_buffer);
esp_h264_dec_close(dec_handle);
esp_h264_dec_del(dec_handle);
fclose(f);
if (result_pixels) {
FreeOSLogI("VID", "Success. Frame extraction complete.");
} else {
FreeOSLogE("VID", "Failed to decode any frame.");
}
return result_pixels;
}
// --- MAIN FUNCTION: GET SPECIFIC FRAME ---
// frameNumber: 0-based index (0 = first frame, 100 = 101st frame)
uint8_t* get_video_frame(const char *filepath, int width, int height, int frameNumber) {
FreeOSLogI("VID", "Seeking Frame %d in %s...", frameNumber, filepath);
FILE *f = fopen(filepath, "rb");
if (f == NULL) {
FreeOSLogE("VID", "File not found");
return NULL;
}
// 1. Setup Decoder
esp_h264_dec_cfg_sw_t cfg = { .pic_type = ESP_H264_RAW_FMT_I420 };
esp_h264_dec_handle_t dec_handle = NULL;
if (esp_h264_dec_sw_new(&cfg, &dec_handle) != ESP_H264_ERR_OK) {
fclose(f);
return NULL;
}
esp_h264_dec_open(dec_handle);
// 2. Allocate Buffer (64KB is usually enough for chunks)
const int BUFFER_CAPACITY = 1024 * 64;
uint8_t *file_buffer = (uint8_t *)heap_caps_malloc(BUFFER_CAPACITY, MALLOC_CAP_SPIRAM);
uint8_t *result_pixels = NULL;
int current_frame_index = 0;
size_t current_data_len = 0;
esp_h264_dec_in_frame_t in_frame = { 0 };
esp_h264_dec_out_frame_t out_frame = { 0 };
// 3. Decode Loop
while (1) {
// Read data into whatever space is left in buffer
size_t space_available = BUFFER_CAPACITY - current_data_len;
// Safety: If buffer full, hard reset logic (similar to your player task)
if (space_available < 1024) {
current_data_len = 0;
space_available = BUFFER_CAPACITY;
}
size_t bytes_read = fread(file_buffer + current_data_len, 1, space_available, f);
if (bytes_read == 0) break; // End of file
current_data_len += bytes_read;
in_frame.raw_data.buffer = file_buffer;
in_frame.raw_data.len = current_data_len;
while (in_frame.raw_data.len > 0) {
esp_h264_err_t ret = esp_h264_dec_process(dec_handle, &in_frame, &out_frame);
// Check if a frame was completed
if (ret == ESP_H264_ERR_OK && out_frame.outbuf != NULL) {
// Is this the frame we want?
if (current_frame_index == frameNumber) {
FreeOSLogI("VID", "Found Frame %d! Converting...", frameNumber);
uint8_t *y = out_frame.outbuf;
uint8_t *u = y + (width * height);
uint8_t *v = u + (width * height / 4);
// Convert ONLY this frame
result_pixels = yuv420_to_rgb888(y, u, v, width, height);
// Cleanup and Exit immediately
goto cleanup;
}
// Not the right frame yet, keep going
current_frame_index++;
}
// Advance pointers
if (in_frame.consume > 0) {
in_frame.raw_data.buffer += in_frame.consume;
in_frame.raw_data.len -= in_frame.consume;
} else {
break; // Fetch more data
}
}
// Preserve leftovers (Move to front)
if (in_frame.raw_data.len > 0) {
memmove(file_buffer, in_frame.raw_data.buffer, in_frame.raw_data.len);
}
current_data_len = in_frame.raw_data.len;
}
cleanup:
free(file_buffer);
esp_h264_dec_close(dec_handle);
esp_h264_dec_del(dec_handle);
fclose(f);
if (result_pixels) {
FreeOSLogI("VID", "Frame extraction success.");
} else {
FreeOSLogE("VID", "Frame %d not found (Video too short?).", frameNumber);
}
return result_pixels;
}
void load_video_poster() {
int w = 320;
int h = 480;
// 1. Get the pixels (Allocates memory in PSRAM)
uint8_t *pixels = get_first_video_frame("/sdcard/output.h264", w, h);
//uint8_t *pixels = get_video_frame("/sdcard/output.h264", w, h, 200);
if (pixels != NULL) {
// 2. Assign to your Framebuffer
// Note: You might need to cast to (void*) depending on your struct definition
fb = (Framebuffer){
.displayWidth = w,
.displayHeight = h,
.pixelData = pixels, // The buffer we just created
.colorMode = COLOR_MODE_BGR888
};
// 3. Draw it!
GraphicsCommand cmd_flush = {
.cmd = CMD_UPDATE_AREA,
.x = 0, .y = 0,
.w = 320, .h = 480
};
QueueSend(g_graphics_queue, &cmd_flush, QUEUE_MAX_DELAY);
printf("load_video_poster...\n");
}
}
bool keyboardTouchEnabled = true;
bool appTouchEnabled = true;
static int64_t last_draw_time = 0;
const int64_t draw_interval_us = 100000; // 0.1 seconds in microseconds scrolls smoothly
//const int64_t draw_interval_us = 050000; // 0.05 seconds in microseconds has lag from sending too many requests to graphics pipeline
void updateTouch(void *pvParameter) {
uint32_t event;
FT6336U_TouchPointType touch_data;
float newOffY;
// This tracks the *actual* hardware state
bool is_currently_pressed = false;
while (1) {
// Wait for an interrupt OR a 50ms timeout
xQueueReceive(touch_intr_queue, &event, pdMS_TO_TICKS(50));
esp_err_t ret = ft6336u_scan(&touch_dev, &touch_data);
if (ret != ESP_OK) {
touch_data.touch_count = 0; // Treat scan error as a release
}
// --- ADD THIS LINE ---
uint16_t current_y = touch_data.touch_count > 0 ? touch_data.tp[0].y : 0;
// --- New State Logic ---
if (touch_data.touch_count > 0) {
// --- Finger is DOWN ---
if (is_currently_pressed == false) {
// This is a NEW press.
is_currently_pressed = true; // 1. Set our local state
g_drag_start_y = current_y; // Anchor the start point
FreeOSLogI(TAG, "Touch Pressed: X=%u, Y=%u", touch_data.tp[0].x, touch_data.tp[0].y);
// 2. Lock and post the "event" for LVGL to consume
xSemaphoreTake(g_touch_mutex, QUEUE_MAX_DELAY);
g_last_x = touch_data.tp[0].x;
g_last_y = touch_data.tp[0].y;
g_last_state = LV_INDEV_STATE_PR; // Set the "event"
xSemaphoreGive(g_touch_mutex);
int text_w = 200;
int text_h = 30;
ColorRGBA blue = {.r = 0, .g = 0, .b = 50, .a = 255};
FreeOSLogI(TAG, "Graphics Command");
int x1 = randNumberTo(320);
int y1 = randNumberTo(480);
x1 = 100;
y1 = 410;
FreeOSLogI(TAG, "Graphics Command %d %d", x1, y1);
if (hasDrawnKeyboard == false) {
hasDrawnKeyboard = true;
// --- 1. Send command to clear the area ---
GraphicsCommand cmd_clear;
cmd_clear.cmd = CMD_DRAW_RECT;
cmd_clear.x = x1;
cmd_clear.y = y1;
cmd_clear.w = text_w;
cmd_clear.h = text_h;
cmd_clear.color = blue;
//QueueSend(g_graphics_queue, &cmd_clear, 0);
ColorRGBA white2 = {.r = 255, .g = 255, .b = 255, .a = 255};
// --- 2. Send command to draw the text ---
GraphicsCommand cmd_text;
cmd_text.cmd = CMD_DRAW_TEXT;
cmd_text.x = x1;
cmd_text.y = y1; // Your renderText needs to handle Y as baseline
cmd_text.text = strdup("Tapped!");
//QueueSend(g_graphics_queue, &cmd_text, 0);
// --- 2. Send command to draw the text ---
GraphicsCommand cmd_text1;
cmd_text1.cmd = CMD_DRAW_TEXT;
cmd_text1.x = 20;
cmd_text1.y = 380; // Your renderText needs to handle Y as baseline
cmd_text1.text = strdup(" Q W E R T Y U I O P");
cmd_text1.color = white2;
QueueSend(g_graphics_queue, &cmd_text1, 0);
// --- 2. Send command to draw the text ---
GraphicsCommand cmd_text12;
cmd_text12.cmd = CMD_DRAW_TEXT;
cmd_text12.x = 20;
cmd_text12.y = 420; // Your renderText needs to handle Y as baseline
cmd_text12.text = strdup(" A S D F G H J K L");
cmd_text12.color = white2;
QueueSend(g_graphics_queue, &cmd_text12, 0);
// --- 2. Send command to draw the text ---
GraphicsCommand cmd_text13;
cmd_text13.cmd = CMD_DRAW_TEXT;
cmd_text13.x = 20;
cmd_text13.y = 460; // Your renderText needs to handle Y as baseline
cmd_text13.text = strdup(" Z X C V B N M");
cmd_text13.color = white2;
QueueSend(g_graphics_queue, &cmd_text13, 0);
const char *long_text = "The quick brown fox jumps over the lazy dog. "
"This text needs to demonstrate word-wrapping and line-breaking "
"within a confined area for our new scroll view. \n\n" // Explicit newline
"This final sentence should appear on a completely new line.";
ColorRGBA text_color = {.r = 255, .g = 255, .b = 255, .a = 255}; // White
// --- 3. Define and Initialize the TextFormat Struct ---
TextFormat text_format = {
// Property 1: Center the text horizontally within the clip_w area
.alignment = TEXT_ALIGN_CENTER,
// Property 2: Never split words mid-line (will move "boundaries" to next line)
.wrapMode = TEXT_WRAP_MODE_WHOLE_WORD,
// Property 3: Add 10 extra pixels of space between the calculated line height
.lineSpacing = -2,
// Property 4: Use FreeType's kerning metrics for a polished look
.glyphSpacing = -1
};
ColorRGBA black_alpha = {.r = 0, .g = 0, .b = 0, .a = 255};
GraphicsCommand cmd_text2;
cmd_text2.cmd = CMD_DRAW_TEXT_BOX;
cmd_text2.x = 20;
cmd_text2.y = 150; // Your renderText needs to handle Y as baseline
cmd_text2.w = 280;
cmd_text2.h = 300;
cmd_text2.color = text_color;
cmd_text2.textFormat = text_format;
cmd_text2.fontSize = 12;
cmd_text2.text = strdup(long_text);
QueueSend(g_graphics_queue, &cmd_text2, 0);
GraphicsCommand cmd_star;
cmd_star.cmd = CMD_DRAW_STAR;
cmd_star.x = 20;
cmd_star.y = 460; // Your renderText needs to handle Y as baseline
//QueueSend(g_graphics_queue, &cmd_star, 0);
GraphicsCommand cmd_update;
cmd_update.cmd = CMD_UPDATE_AREA;
cmd_update.x = 0;
cmd_update.y = 0; // A rough bounding box
cmd_update.w = 320;
cmd_update.h = 480; // A bit larger to be safe
QueueSend(g_graphics_queue, &cmd_update, 0);
if (!addedCursor) {
addedCursor = true;
if (setup_cursor_buffers() != ESP_OK) {
FreeOSLogE(TAG, "FATAL: Failed to set up cursor backup buffers. Halting task.");
vTaskDelete(NULL);
return;
}
FreeOSLogI(TAG, "Cursor backup buffer allocated.");
// 2. Build the CMD_CURSOR_SETUP command
GraphicsCommand cmd_cursor_setup;
cmd_cursor_setup.cmd = CMD_CURSOR_SETUP;
// Set the exact location here:
cmd_cursor_setup.x = 100;
cmd_cursor_setup.y = 100;
// Pass the cursor's fixed size in case the graphics task needs it
cmd_cursor_setup.w = CURSOR_W;
cmd_cursor_setup.h = CURSOR_H;
// 3. Send the command to initiate the blink cycle
//QueueSend(g_graphics_queue, &cmd_cursor_setup, 0);
}
}
else {
ColorRGBA black_alpha = {.r = 0, .g = 0, .b = 0, .a = 255};
GraphicsCommand cmd_text;
cmd_text.cmd = CMD_DRAW_TEXT;
cmd_text.x = 30+keyboardCursorPosition*20;
cmd_text.y = 53; // Your renderText needs to handle Y as baseline
cmd_text.color = black_alpha;
cmd_text.text = strdup(letterForCoordinate());
keyboardCursorPosition++;
GraphicsCommand cmd_update;
cmd_update.cmd = CMD_UPDATE_AREA;
cmd_update.x = 0;
cmd_update.y = 0; // A rough bounding box
cmd_update.w = 320;
cmd_update.h = 100; // A bit larger to be safe
//QueueSend(g_graphics_queue, &cmd_update, 0);
if (!setupui) {
// 1. Create the high-level View Objects (RAM only)
// Make sure setup_ui_demo assigns to the global 'mainWindowView'
setup_ui_demo();
// 2. Trigger the first render (Generates commands)
//update_full_ui();
setupui = true;
}
FreeOSLogI(TAG, "app_main() finished. Tasks are running.");
}
}
else {
FreeOSLogI(TAG, "touch 3");
int curr_y = touch_data.tp[0].y;
if (myDemoTextView != NULL) {
g_active_scrollview = myDemoTextView->scrollView;
if (notFirstTouch == false) {
notFirstTouch = true;
g_touch_last_y = curr_y;
lastOffY = g_active_scrollview->contentOffset->y;
FreeOSLogI(TAG, "set lastOffY %d", lastOffY);
}
if (g_active_scrollview) {
int delta = g_touch_last_y - curr_y; // Drag Up = Positive Delta
FreeOSLogI(TAG, "g_active_scrollview %d", delta);
// Calculate new offset
newOffY = lastOffY + (float)delta;
float yValue = (float)lastOffY;
CCPoint offsetPt;
offsetPt.x = 0;
offsetPt.y = (float)lastOffY;
lastOffY = delta;
CCPoint targetPoint = {
.x = 0,
.y = lastOffY // Correctly assigns the float value
};
// Apply offset (This function handles clamping and moving the view)
//CCPoint offsetPt = {.x = 0, .y = yValue};
FreeOSLogI(TAG, "g_active_scrollview1 %f %f %f", lastOffY, newOffY, yValue);
scrollViewSetContentOffset(g_active_scrollview, &targetPoint);//ccPoint(0, lastOffY)
FreeOSLogI(TAG, "g_active_scrollview contentOffset %f", g_active_scrollview->contentOffset->y);
// 2. THROTTLING CHECK: Only draw if 0.2s has passed
int64_t now = esp_timer_get_time();
if ((now - last_draw_time) > draw_interval_us) {
// Send the expensive command
update_view_only(g_active_scrollview->view);
FreeOSLogI(TAG, "g_active_scrollview contentOffset %f", g_active_scrollview->contentOffset->y);
// Reset the timer
last_draw_time = now;
}
}
}
}
// If finger is still down, we do nothing.
} else {
// --- Finger is UP ---
if (is_currently_pressed == true) {
// This is a NEW release.
is_currently_pressed = false; // 1. Set our local state
notFirstTouch = false;
if (g_active_scrollview) FreeOSLogI(TAG, "Touch Released");
}
}
if (touchEnabled) {
if (touch_data.touch_count > 0 && setupui == true) {
int touchX = touch_data.tp[0].x;
int touchY = touch_data.tp[0].y;
is_currently_pressed = true; // 1. Set our local state
FreeOSLogI(TAG, "Touch DOWN");
// If keyboard is visible (check if uiKeyboardView != NULL)
if (uiKeyboardView != NULL && keyboardTouchEnabled == true) {
if (touchY > uiKeyboardView->frame->origin->y) {
keyboardTouchEnabled = false;
handle_keyboard_touch(touchX, touchY);
printf("handle handle_keyboard_touch");
//return; // Swallow touch so nothing underneath gets clicked
}
}
}
else {
keyboardTouchEnabled = true;
}
if (touch_data.touch_count == 0) {
is_currently_pressed = false;
//FreeOSLogI(TAG, "Touch UP");
if (currentView == CurrentViewFiles) {
handle_files_touch(0, 0, is_currently_pressed);
}
if (currentView == CurrentViewClock) {
printf("CurrentViewClock");
handle_clock_touch(0, 0, is_currently_pressed);
}
if (currentView == CurrentViewPaint) {
handle_paint_touch(0, 0, is_currently_pressed);
}
if (currentView == CurrentViewMusic) {
handle_music_touch(0, 0, is_currently_pressed);
}
}
//printf("touch3");
if (currentView == CurrentViewHome) {
if (touch_data.touch_count > 0 && setupui == true) {
int curr_x = touch_data.tp[0].x;
int curr_y = touch_data.tp[0].y;
checkAvailableMemory();
CCView* touchedView = find_grid_item_at(curr_x, curr_y);
CCPoint parentAbs = getAbsoluteOrigin(touchedView);
if (touchedView && touchedView != g_last_touched_icon && openedApp == false) {
if (g_last_touched_icon != NULL) {
if (g_last_touched_icon->backgroundColor) free(g_last_touched_icon->backgroundColor);
g_last_touched_icon->backgroundColor = color(0,0,0,0);
update_view_area_via_parent(g_last_touched_icon);
}
if (touchedView->backgroundColor) free(touchedView->backgroundColor);
touchedView->backgroundColor = color(0.0, 0.0, 0.0, 0.2);
update_view_only(touchedView);
printf("updatevie1w");
openHomeMenuItem(touchedView->tag);
g_last_touched_icon = touchedView;
}
}
else {
// --- FINGER UP (Release) ---
if (g_last_touched_icon != NULL && openedApp == false) {
if (g_last_touched_icon->backgroundColor) free(g_last_touched_icon->backgroundColor);
g_last_touched_icon->backgroundColor = color(0,0,0,0);
update_view_area_via_parent(g_last_touched_icon);
g_last_touched_icon = NULL;
}
is_currently_pressed = false;
}
}
else if (currentView == CurrentViewFiles || currentView == CurrentViewSettings || currentView == CurrentViewCalculator || currentView == CurrentViewMusic || currentView == CurrentViewPhotos || currentView == CurrentViewText || currentView == CurrentViewClock || currentView == CurrentViewPaint) {
//else if (currentView == CurrentViewFiles || currentView == CurrentViewSettings)
if (touch_data.touch_count > 0 && setupui == true) {
int x = touch_data.tp[0].x;
int y = touch_data.tp[0].y;
checkAvailableMemory();
if (x < 40 && y < 30) {
close_current_app();
}
if (currentView == CurrentViewFiles) {
handle_files_touch(x, y, is_currently_pressed);
}
if (currentView == CurrentViewSettings) {
handle_settings_touch(x, y, 1);
}
if (currentView == CurrentViewPhotos) {
CCView* row = find_subview_at_point(mainWindowView, x, y);
if (row) {
handle_gallery_touch(row->tag);
}
}
if (currentView == CurrentViewCalculator) {
CCView* row = find_subview_at_point(mainWindowView, x, y);
if (row) {
handle_calculator_input(row->tag);
}
}
if (currentView == CurrentViewClock) {
printf("CurrentViewClock");
handle_clock_touch(x, y, is_currently_pressed);
}
if (currentView == CurrentViewPaint) {
handle_paint_touch(x, y, is_currently_pressed);
}
if (currentView == CurrentViewMusic) {
handle_music_touch(x, y, is_currently_pressed);
}
}
else {
}
}
else if (currentView == CurrentViewWifi) {
if (touch_data.touch_count > 0 && setupui == true) {
int x = touch_data.tp[0].x;
int y = touch_data.tp[0].y;
checkAvailableMemory();
if (appTouchEnabled == true) {
appTouchEnabled = false;
handle_wifi_touch(x, y);
}
}
else {
appTouchEnabled = true;
}
}
}
}
}
// ----------------------------------------------------------------------
// INITIALIZATION FUNCTION
// Call this function from your app_main
// ----------------------------------------------------------------------
/*void initialize_touch()
{
FreeOSLogI(TAG, "Initializing I2C Master for touch panel");
// --- 1. Create the new I2C Master Bus ---
i2c_master_bus_config_t bus_cfg = {
.i2c_port = I2C_HOST,
.scl_io_num = PIN_NUM_I2C_SCL,
.sda_io_num = PIN_NUM_I2C_SDA,
.clk_source = I2C_CLK_SRC_DEFAULT,
.glitch_ignore_cnt = 7,
.flags.enable_internal_pullup = true,
};
i2c_master_bus_handle_t bus_handle;
ESP_ERROR_CHECK(i2c_new_master_bus(&bus_cfg, &bus_handle));
// --- 2. Create the I2C Panel IO Handle (THE MISSING STEP) ---
FreeOSLogI(TAG, "Creating I2C panel IO handle");
esp_lcd_panel_io_handle_t io_handle = NULL;
esp_lcd_panel_io_i2c_config_t io_config = ESP_LCD_TOUCH_IO_I2C_FT6x36_CONFIG();
io_config.scl_speed_hz = 400000;
// This is the line you were missing.
// It creates the actual IO device handle.
ESP_ERROR_CHECK(esp_lcd_new_panel_io_i2c(bus_handle, &io_config, &io_handle));
// --- 3. Configure and Initialize the Touch Controller ---
esp_lcd_touch_config_t tp_cfg = {
.x_max = LCD_H_RES,
.y_max = LCD_V_RES,
.rst_gpio_num = -1,//PIN_NUM_TOUCH_RST,
.int_gpio_num = PIN_NUM_TOUCH_INT,
.levels = {
.reset = 0,
.interrupt = 0,
},
.flags = {
.swap_xy = 0,
.mirror_x = 0,
.mirror_y = 0,
},
};
esp_lcd_touch_handle_t tp; // <-- Your handle is named 'tp'
// Now you are passing the *valid* io_handle
esp_err_t err = esp_lcd_touch_new_i2c_ft6x36(io_handle, &tp_cfg, &tp);
if (err != ESP_OK) {
FreeOSLogE(TAG, "Touch panel init failed, continuing without touch...");
} else {
FreeOSLogI(TAG, "Touch panel initialized successfully");
// --- 4. Register Touch Input with LVGL ---
FreeOSLogI(TAG, "Registering touch input with LVGL");
lvgl_touch_indev = lv_indev_create();
lv_indev_set_type(lvgl_touch_indev, LV_INDEV_TYPE_POINTER);
lv_indev_set_read_cb(lvgl_touch_indev, lvgl_touch_cb);
// *** FIX 2 (Typo) ***
// Pass the handle you just created, which is 'tp', not 'tp_handle'
lv_indev_set_user_data(lvgl_touch_indev, tp);
// (Rest of your LVGL code...)
}
}*/
//Joystick
// Define the GPIOs and their corresponding ADC Channels
#define JOY_X_PIN ADC_CHANNEL_3 // GPIO4 corresponds to ADC1 Channel 3
#define JOY_Y_PIN ADC_CHANNEL_4 // GPIO5 corresponds to ADC1 Channel 4
// ADC handle and configuration structures
adc_oneshot_unit_handle_t adc1_handle;
#define DEFAULT_SCAN_LIST_SIZE CONFIG_EXAMPLE_SCAN_LIST_SIZE
#ifdef CONFIG_EXAMPLE_USE_SCAN_CHANNEL_BITMAP
#define USE_CHANNEL_BITMAP 1
#define CHANNEL_LIST_SIZE 3
static uint8_t channel_list[CHANNEL_LIST_SIZE] = {1, 6, 11};
#endif /*CONFIG_EXAMPLE_USE_SCAN_CHANNEL_BITMAP*/
static void print_auth_mode(int authmode)
{
switch (authmode) {
case WIFI_AUTH_OPEN:
FreeOSLogI(TAG, "Authmode \tWIFI_AUTH_OPEN");
break;
case WIFI_AUTH_OWE:
FreeOSLogI(TAG, "Authmode \tWIFI_AUTH_OWE");
break;
case WIFI_AUTH_WEP:
FreeOSLogI(TAG, "Authmode \tWIFI_AUTH_WEP");
break;
case WIFI_AUTH_WPA_PSK:
FreeOSLogI(TAG, "Authmode \tWIFI_AUTH_WPA_PSK");
break;
case WIFI_AUTH_WPA2_PSK:
FreeOSLogI(TAG, "Authmode \tWIFI_AUTH_WPA2_PSK");
break;
case WIFI_AUTH_WPA_WPA2_PSK:
FreeOSLogI(TAG, "Authmode \tWIFI_AUTH_WPA_WPA2_PSK");
break;
case WIFI_AUTH_ENTERPRISE:
FreeOSLogI(TAG, "Authmode \tWIFI_AUTH_ENTERPRISE");
break;
case WIFI_AUTH_WPA3_PSK:
FreeOSLogI(TAG, "Authmode \tWIFI_AUTH_WPA3_PSK");
break;
case WIFI_AUTH_WPA2_WPA3_PSK:
FreeOSLogI(TAG, "Authmode \tWIFI_AUTH_WPA2_WPA3_PSK");
break;
case WIFI_AUTH_WPA3_ENTERPRISE:
FreeOSLogI(TAG, "Authmode \tWIFI_AUTH_WPA3_ENTERPRISE");
break;
case WIFI_AUTH_WPA2_WPA3_ENTERPRISE:
FreeOSLogI(TAG, "Authmode \tWIFI_AUTH_WPA2_WPA3_ENTERPRISE");
break;
case WIFI_AUTH_WPA3_ENT_192:
FreeOSLogI(TAG, "Authmode \tWIFI_AUTH_WPA3_ENT_192");
break;
default:
FreeOSLogI(TAG, "Authmode \tWIFI_AUTH_UNKNOWN");
break;
}
}
static void print_cipher_type(int pairwise_cipher, int group_cipher)
{
switch (pairwise_cipher) {
case WIFI_CIPHER_TYPE_NONE:
FreeOSLogI(TAG, "Pairwise Cipher \tWIFI_CIPHER_TYPE_NONE");
break;
case WIFI_CIPHER_TYPE_WEP40:
FreeOSLogI(TAG, "Pairwise Cipher \tWIFI_CIPHER_TYPE_WEP40");
break;
case WIFI_CIPHER_TYPE_WEP104:
FreeOSLogI(TAG, "Pairwise Cipher \tWIFI_CIPHER_TYPE_WEP104");
break;
case WIFI_CIPHER_TYPE_TKIP:
FreeOSLogI(TAG, "Pairwise Cipher \tWIFI_CIPHER_TYPE_TKIP");
break;
case WIFI_CIPHER_TYPE_CCMP:
FreeOSLogI(TAG, "Pairwise Cipher \tWIFI_CIPHER_TYPE_CCMP");
break;
case WIFI_CIPHER_TYPE_TKIP_CCMP:
FreeOSLogI(TAG, "Pairwise Cipher \tWIFI_CIPHER_TYPE_TKIP_CCMP");
break;
case WIFI_CIPHER_TYPE_AES_CMAC128:
FreeOSLogI(TAG, "Pairwise Cipher \tWIFI_CIPHER_TYPE_AES_CMAC128");
break;
case WIFI_CIPHER_TYPE_SMS4:
FreeOSLogI(TAG, "Pairwise Cipher \tWIFI_CIPHER_TYPE_SMS4");
break;
case WIFI_CIPHER_TYPE_GCMP:
FreeOSLogI(TAG, "Pairwise Cipher \tWIFI_CIPHER_TYPE_GCMP");
break;
case WIFI_CIPHER_TYPE_GCMP256:
FreeOSLogI(TAG, "Pairwise Cipher \tWIFI_CIPHER_TYPE_GCMP256");
break;
default:
FreeOSLogI(TAG, "Pairwise Cipher \tWIFI_CIPHER_TYPE_UNKNOWN");
break;
}
switch (group_cipher) {
case WIFI_CIPHER_TYPE_NONE:
FreeOSLogI(TAG, "Group Cipher \tWIFI_CIPHER_TYPE_NONE");
break;
case WIFI_CIPHER_TYPE_WEP40:
FreeOSLogI(TAG, "Group Cipher \tWIFI_CIPHER_TYPE_WEP40");
break;
case WIFI_CIPHER_TYPE_WEP104:
FreeOSLogI(TAG, "Group Cipher \tWIFI_CIPHER_TYPE_WEP104");
break;
case WIFI_CIPHER_TYPE_TKIP:
FreeOSLogI(TAG, "Group Cipher \tWIFI_CIPHER_TYPE_TKIP");
break;
case WIFI_CIPHER_TYPE_CCMP:
FreeOSLogI(TAG, "Group Cipher \tWIFI_CIPHER_TYPE_CCMP");
break;
case WIFI_CIPHER_TYPE_TKIP_CCMP:
FreeOSLogI(TAG, "Group Cipher \tWIFI_CIPHER_TYPE_TKIP_CCMP");
break;
case WIFI_CIPHER_TYPE_SMS4:
FreeOSLogI(TAG, "Group Cipher \tWIFI_CIPHER_TYPE_SMS4");
break;
case WIFI_CIPHER_TYPE_GCMP:
FreeOSLogI(TAG, "Group Cipher \tWIFI_CIPHER_TYPE_GCMP");
break;
case WIFI_CIPHER_TYPE_GCMP256:
FreeOSLogI(TAG, "Group Cipher \tWIFI_CIPHER_TYPE_GCMP256");
break;
default:
FreeOSLogI(TAG, "Group Cipher \tWIFI_CIPHER_TYPE_UNKNOWN");
break;
}
}
#ifdef USE_CHANNEL_BITMAP
static void array_2_channel_bitmap(const uint8_t channel_list[], const uint8_t channel_list_size, wifi_scan_config_t *scan_config) {
for(uint8_t i = 0; i < channel_list_size; i++) {
uint8_t channel = channel_list[i];
scan_config->channel_bitmap.ghz_2_channels |= (1 << channel);
}
}
#endif /*USE_CHANNEL_BITMAP*/
/* Initialize Wi-Fi as sta and set scan method */
static void wifi_scan(void)
{
ESP_ERROR_CHECK(esp_netif_init());
ESP_ERROR_CHECK(esp_event_loop_create_default());
esp_netif_t *sta_netif = esp_netif_create_default_wifi_sta();
assert(sta_netif);
wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
ESP_ERROR_CHECK(esp_wifi_init(&cfg));
uint16_t number = DEFAULT_SCAN_LIST_SIZE;
wifi_ap_record_t ap_info[DEFAULT_SCAN_LIST_SIZE];
uint16_t ap_count = 0;
memset(ap_info, 0, sizeof(ap_info));
ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA));
ESP_ERROR_CHECK(esp_wifi_start());
#ifdef USE_CHANNEL_BITMAP
wifi_scan_config_t *scan_config = (wifi_scan_config_t *)calloc(1,sizeof(wifi_scan_config_t));
if (!scan_config) {
FreeOSLogE(TAG, "Memory Allocation for scan config failed!");
return;
}
array_2_channel_bitmap(channel_list, CHANNEL_LIST_SIZE, scan_config);
esp_wifi_scan_start(scan_config, true);
free(scan_config);
#else
esp_wifi_scan_start(NULL, true);
#endif /*USE_CHANNEL_BITMAP*/
FreeOSLogI(TAG, "Max AP number ap_info can hold = %u", number);
ESP_ERROR_CHECK(esp_wifi_scan_get_ap_num(&ap_count));
ESP_ERROR_CHECK(esp_wifi_scan_get_ap_records(&number, ap_info));
FreeOSLogI(TAG, "Total APs scanned = %u, actual AP number ap_info holds = %u", ap_count, number);
for (int i = 0; i < number; i++) {
FreeOSLogI(TAG, "SSID \t\t%s", ap_info[i].ssid);
FreeOSLogI(TAG, "RSSI \t\t%d", ap_info[i].rssi);
print_auth_mode(ap_info[i].authmode);
if (ap_info[i].authmode != WIFI_AUTH_WEP) {
print_cipher_type(ap_info[i].pairwise_cipher, ap_info[i].group_cipher);
}
FreeOSLogI(TAG, "Channel \t\t%d", ap_info[i].primary);
}
}
/**
* @brief Initializes and mounts the FATFS partition using the Wear Leveling driver.
*/
void mount_fatfs(void) {
// Configuration for the mount (Ensure CONFIG_WL_SECTOR_SIZE is set in menuconfig)
/*const esp_vfs_fat_mount_config_t mount_config = {
.max_files = 10000,
.format_if_mount_failed = false, // Format if initial mount fails (enables writing)
.allocation_unit_size = CONFIG_WL_SECTOR_SIZE
};*/
esp_vfs_fat_sdmmc_mount_config_t mount_config = {
.format_if_mount_failed = false,
.max_files = 100
};
FreeOSLogI(TAG, "Mounting FATFS partition '%s' at %s...", PARTITION_LABEL, MOUNT_POINT);
// Mount the FAT partition with Read/Write (RW) and Wear Leveling (WL) support
/*esp_err_t ret = esp_vfs_fat_spiflash_mount_rw_wl(
MOUNT_POINT, // Base path (e.g., /fat)
PARTITION_LABEL, // Partition Label (e.g., fat_data)
&mount_config,
&wl_handle // Wear Leveling handle (output)
);*/
esp_err_t ret = esp_vfs_fat_spiflash_mount_ro("/spiflash", "storage", &mount_config);
if (ret != ESP_OK) {
FreeOSLogE(TAG, "Failed to mount FATFS (%s)", esp_err_to_name(ret));
} else {
FreeOSLogI(TAG, "FATFS mounted successfully.");
}
}
void checkAvailableMemory() {
size_t free_heap_size = heap_caps_get_free_size(MALLOC_CAP_8BIT);
printf("Free heap size: %zu bytes\n", free_heap_size);
size_t free_heap_size1 = heap_caps_get_free_size(MALLOC_CAP_SPIRAM);
printf("Free heap size spiram: %zu bytes\n", free_heap_size1);
size_t free_heap_size2 = heap_caps_get_free_size(MALLOC_CAP_EXEC);
printf("Free heap size MALLOC_CAP_EXEC: %zu bytes\n", free_heap_size2);
// Optionally, you can print other heap info
size_t free_internal_heap_size = heap_caps_get_free_size(MALLOC_CAP_INTERNAL);
printf("Free internal heap size: %zu bytes\n", free_internal_heap_size);
printf("Free heap size: %zu bytes\n", heap_caps_get_free_size(MALLOC_CAP_8BIT));
FreeOSLogI(TAG, "Free heap size: %zu bytes\n", free_heap_size);
FreeOSLogI(TAG, "Free internal heap size: %zu bytes\n", free_internal_heap_size);
//FreeOSLogI(TAG, "EXECUTABLE 2\n");
}
// Function to draw a block of pixels (240x240)
void raw_drawing_task(void *pvParameter)
{
FreeOSLogI(TAG, "Starting raw drawing demo.");
// Create a pixel buffer for the entire screen (240 * 240 pixels * 2 bytes/pixel)
// This is a large buffer, ensure your PSRAM is enabled/available.
// If you don't have enough RAM/PSRAM, reduce the size or use a small tile.
uint16_t *color_data = (uint16_t *)heap_caps_malloc(LCD_H_RES * LCD_V_RES * sizeof(uint16_t), MALLOC_CAP_DMA);
if (!color_data) {
FreeOSLogE(TAG, "Failed to allocate display buffer!");
vTaskDelete(NULL);
return;
}
// Fill the buffer with the color red (0xF800)
for (int i = 0; i < LCD_H_RES * LCD_V_RES; i++) {
color_data[i] = LCD_COLOR_RED;
}
// Draw the bitmap once per second
while (1) {
// Function: esp_lcd_panel_draw_bitmap(panel_handle, x_start, y_start, x_end, y_end, color_data)
ESP_ERROR_CHECK(esp_lcd_panel_draw_bitmap(
panel_handle,
0, // x_start
0, // y_start
LCD_H_RES, // x_end (Exclusive, so 240 is 0-239)
LCD_V_RES, // y_end (Exclusive, so 240 is 0-239)
color_data
));
FreeOSLogI(TAG, "Drew a red screen.");
vTaskDelay(pdMS_TO_TICKS(1000));
}
// Note: In a real app, you would free the memory before deleting the task.
}
/*void lvgl_ui_task(void *pvParameter) {
// *** LVGL UI Creation Code Goes Here ***
lv_obj_t *label = lv_label_create(lv_screen_active());
lv_label_set_text(label, "LVGL is Running!");
lv_obj_center(label);
// LVGL main loop handler
while(1) {
lv_timer_handler(); // Processes all LVGL tasks and redrawing
vTaskDelay(pdMS_TO_TICKS(5)); // Wait 5ms
}
}*/
// Style for small, normal weight text (e.g., 18px)
static lv_style_t style_normal;
// Style for large, bold/heavy text (e.g., 36px)
static lv_style_t style_heavy;
// Style for medium, light/thin text (e.g., 24px)
static lv_style_t style_light;
/**
* @brief Initialize all custom LVGL styles.
*/
static void init_custom_styles()
{
// --- Normal Style (Montserrat 18) ---
lv_style_init(&style_normal);
// Use the built-in Montserrat font with size 18 and a medium/normal weight.
lv_style_set_text_font(&style_normal, &lv_font_montserrat_8);
lv_style_set_text_color(&style_normal, lv_color_white());
// --- Heavy Style (Montserrat 36) ---
lv_style_init(&style_heavy);
// Use the largest, heaviest weight available (Montserrat 36)
lv_style_set_text_font(&style_heavy, &lv_font_montserrat_10);
lv_style_set_text_color(&style_heavy, lv_color_hex(0xFFDD00)); // Yellowish
// --- Light Style (Montserrat 24) ---
lv_style_init(&style_light);
// Use Montserrat 24. Since LVGL doesn't have explicit 'light' weight files
// for all sizes, we use the default and often customize it further if needed.
// However, 24px is a distinct size from 18 and 36.
lv_style_set_text_font(&style_light, &lv_font_montserrat_12);
lv_style_set_text_color(&style_light, lv_color_hex(0xAAAAAA)); // Gray
}
// -----------------------------------------------------------
// 2. UI TASK FUNCTION
// -----------------------------------------------------------
/**
* @brief LVGL UI creation and handler task.
*/
void lvgl_ui_task(void *pvParameter)
{
FreeOSLogI(TAG, "LVGL UI Task started.");
// Lock the LVGL mutex before accessing any LVGL objects
lvgl_port_lock(0);
// 1. Initialize styles
init_custom_styles();
lv_obj_t *scr = lv_screen_active();
lv_obj_set_style_bg_color(scr, lv_color_hex(0x002244), 0); // Dark Blue background
// --- LABEL 1: Large & Heavy ---
lv_obj_t *label1 = lv_label_create(scr);
lv_label_set_text(label1, "SYSTEM ONLINE");
lv_obj_add_style(label1, &style_heavy, 0); // Apply the heavy style
lv_obj_align(label1, LV_ALIGN_TOP_MID, 0, 10);
// --- LABEL 2: Medium & Light ---
lv_obj_t *label2 = lv_label_create(scr);
lv_label_set_text(label2, "Meet with sales team");
lv_obj_add_style(label2, &style_light, 0); // Apply the light style
// Align below Label 1
lv_obj_align_to(label2, label1, LV_ALIGN_OUT_BOTTOM_MID, 0, 10);
// --- LABEL 3: Small & Normal ---
lv_obj_t *label3 = lv_label_create(scr);
lv_label_set_text(label3, "Powered by ESP-IDF and LVGL");
lv_obj_add_style(label3, &style_normal, 0); // Apply the normal style
// Align below Label 2
lv_obj_align_to(label3, label2, LV_ALIGN_OUT_BOTTOM_MID, 0, 10);
// Unlock the LVGL mutex
lvgl_port_unlock();
// -----------------------------------------------------------
// LVGL Main Loop
// -----------------------------------------------------------
while(1) {
// Must protect lv_timer_handler() with lock/unlock
lvgl_port_lock(0);
lv_timer_handler(); // Processes all LVGL tasks, redrawing, and animations
lvgl_port_unlock();
vTaskDelay(pdMS_TO_TICKS(5));
}
}
// Styles (reusing simple styles, assume default LVGL fonts are available)
static lv_style_t style_main_bg;
/**
* @brief Initializes base styles for the demo.
*/
static void init_kb_styles()
{
lv_style_init(&style_main_bg);
// Set background color for the screen (or container)
lv_style_set_bg_color(&style_main_bg, lv_color_hex(0xEEEEEE));
lv_style_set_text_color(&style_main_bg, lv_color_black());
}
// Global variables to store the latest joystick values (access must be thread-safe)
volatile int g_joystick_x = 0;
volatile int g_joystick_y = 0;
// Mutex to protect access to the global variables
SemaphoreHandle_t joystick_mutex;
lv_obj_t *g_cursor_obj;
volatile lv_coord_t g_cursor_x_pos; // Manual cursor position tracker
volatile lv_coord_t g_cursor_y_pos; // Manual cursor position tracker
// Joystick ADC Range
#define JOY_X_MIN 1000
#define JOY_X_MAX 3200
#define JOY_Y_MIN 600
#define JOY_Y_MAX 3600
#define JOY_X_LEFT 1100 //200 to 1100, move left if less than 1100
#define JOY_X_NEUTRAL 1600 //1500 to 2400, do not move cursor on x axis if joystick x between these values
#define JOY_X_RIGHT 2600 //2800 to 3600, move right if greater than 2800
#define JOY_Y_DOWN 1400 //600 to 1200, move down if less than 1200
#define JOY_Y_NEUTRAL 1800 //1700 to 2600, do not move cursor on y axis if joystick y between these values
#define JOY_Y_UP 2800 //2800 to 4000, move up if greater than 2800
// Screen Resolution (Adjust these to your actual display size)
#define SCREEN_WIDTH 240 // Example: 320 pixels wide
#define SCREEN_HEIGHT 320 // Example: 240 pixels high
/**
* @brief LVGL UI creation and handler task, focusing on the keyboard.
*/
void lvgl_ui_task1(void *pvParameter)
{
FreeOSLogI(TAG, "LVGL Keyboard Demo Task started.");
//lvgl_port_lock(0);
lv_lock();
init_kb_styles();
lv_obj_t *scr = lv_screen_active();
lv_obj_add_style(scr, &style_main_bg, 0);
// 1. Create a Text Area (Input Field)
// This is where the user's input will appear.
lv_obj_t *ta = lv_textarea_create(scr);
lv_textarea_set_text_selection(ta, true);
// Set the size and position of the text area
lv_obj_set_width(ta, lv_pct(90));
lv_obj_set_height(ta, LV_SIZE_CONTENT);
lv_obj_align(ta, LV_ALIGN_TOP_MID, 0, 10);
lv_textarea_set_placeholder_text(ta, "Tap here to type...");
lv_textarea_set_one_line(ta, true); // Keep it simple
// 2. Create the Keyboard
// This creates the virtual keyboard widget.
lv_obj_t *kb = lv_keyboard_create(scr);
// Set the size: full width and align it to the bottom
lv_obj_set_width(kb, LV_PCT(100));
lv_obj_set_height(kb, lv_pct(50));
lv_obj_align(kb, LV_ALIGN_BOTTOM_MID, 0, 0);
// 3. Link the Keyboard to the Text Area
// This tells the keyboard where to send its input.
lv_keyboard_set_textarea(kb, ta);
// 4. Set Initial Focus
// Set the text area as the initially focused object so the keyboard is ready.
lv_obj_add_state(ta, LV_STATE_FOCUSED);
// Global handle for the cursor object (declared outside of the function)
g_cursor_obj = NULL;
// --- Cursor Style ---
lv_style_t style_cursor;
lv_style_init(&style_cursor);
lv_style_set_bg_color(&style_cursor, lv_color_white()); // Small white circle
lv_style_set_border_width(&style_cursor, 2);
lv_style_set_border_color(&style_cursor, lv_color_black()); // Black border
lv_style_set_radius(&style_cursor, LV_RADIUS_CIRCLE); // Makes it a perfect circle
lv_style_set_pad_all(&style_cursor, 0); // No padding
// --- Cursor Object Creation ---
g_cursor_obj = lv_obj_create(scr); // Create as a simple object on the screen
lv_obj_set_size(g_cursor_obj, 10, 10); // Set size (e.g., 10x10 pixels)
lv_obj_add_style(g_cursor_obj, &style_cursor, 0);
lv_obj_set_pos(g_cursor_obj, SCREEN_WIDTH / 2, SCREEN_HEIGHT / 2); // Start at the center
lv_obj_move_foreground(g_cursor_obj); // Ensures the cursor is drawn on top
//lvgl_port_unlock();
lv_unlock();
//vTaskDelete(NULL);
// LVGL Main Loop
while(1) {
lv_lock();
lv_timer_handler();
lv_unlock();
vTaskDelay(pdMS_TO_TICKS(5));
}
// LVGL Main Loop
// LVGL Main Loop
//while(1) {
/*
// --- 1. Read Joystick Position Safely (RAW VALUES) ---
int raw_x = g_joystick_x; // Initialize with current global value
int raw_y = g_joystick_y;
// Safely retrieve the latest global values
if (xSemaphoreTake(joystick_mutex, QUEUE_MAX_DELAY) == pdTRUE) {
raw_x = g_joystick_x;
raw_y = g_joystick_y;
xSemaphoreGive(joystick_mutex);
}
// --- 2. Rate Control Logic (Analog to Digital Step) ---
int move_x = 0;
int move_y = 0;
// --- X-Axis (Horizontal) ---
if (raw_x < JOY_X_LEFT) {
move_x = -1; // Move Left
} else if (raw_x > JOY_X_RIGHT) {
move_x = 1; // Move Right
}
// --- Y-Axis (Vertical) ---
// Note: LVGL Y=0 is TOP. Y_UP (high ADC) should move cursor toward Y=0 (move_y = -1).
if (raw_y > JOY_Y_UP) {
move_y = -1; // Move UP (towards Y=0)
} else if (raw_y < JOY_Y_DOWN) {
move_y = 1; // Move DOWN (towards Y=HEIGHT)
}
// --- 3. Acquire the LVGL Lock, Update Position, and Handle Tick ---
lvgl_port_lock(0);
if (g_cursor_obj != NULL) {
// Check if movement is needed
if (move_x != 0 || move_y != 0) {
// Update the cursor's internal tracked position
g_cursor_x_pos += move_x;
g_cursor_y_pos += move_y;
// Clamp position to screen boundaries (ensures cursor stays visible)
const int cursor_center_offset = 5; // For a 10x10 cursor
if (g_cursor_x_pos < cursor_center_offset) g_cursor_x_pos = cursor_center_offset;
if (g_cursor_x_pos > SCREEN_WIDTH - cursor_center_offset) g_cursor_x_pos = SCREEN_WIDTH - cursor_center_offset;
if (g_cursor_y_pos < cursor_center_offset) g_cursor_y_pos = cursor_center_offset;
if (g_cursor_y_pos > SCREEN_HEIGHT - cursor_center_offset) g_cursor_y_pos = SCREEN_HEIGHT - cursor_center_offset;
// Set the LVGL object position
lv_obj_set_pos(g_cursor_obj, g_cursor_x_pos - cursor_center_offset, g_cursor_y_pos - cursor_center_offset);
}
}
lv_timer_handler(); // Handle the LVGL tick
lvgl_port_unlock();
// Control cursor speed by yielding (5ms delay = 200 updates per second)
vTaskDelay(pdMS_TO_TICKS(5));*/
//lv_timer_handler();
//vTaskDelay(pdMS_TO_TICKS(5));
//}
}
void joystick_task(void *pvParameter)
{
// --- 1. ADC Unit Initialization ---
adc_oneshot_unit_init_cfg_t init_config = {
.unit_id = ADC_UNIT_1, // Use ADC1
.clk_src = ADC_DIGI_CLK_SRC_DEFAULT,
.ulp_mode = ADC_ULP_MODE_DISABLE,
};
ESP_ERROR_CHECK(adc_oneshot_new_unit(&init_config, &adc1_handle));
// --- 2. ADC Channel Configuration (X-Axis: GPIO4) ---
adc_oneshot_chan_cfg_t chan_config_x = {
.atten = ADC_ATTEN_DB_11, // Attenuation for 0V ~ 3.3V range
.bitwidth = ADC_BITWIDTH_DEFAULT, // Default is 12-bit (0-4095)
};
ESP_ERROR_CHECK(adc_oneshot_config_channel(adc1_handle, JOY_X_PIN, &chan_config_x));
// --- 3. ADC Channel Configuration (Y-Axis: GPIO5) ---
adc_oneshot_chan_cfg_t chan_config_y = {
.atten = ADC_ATTEN_DB_11, // Same attenuation
.bitwidth = ADC_BITWIDTH_DEFAULT,
};
ESP_ERROR_CHECK(adc_oneshot_config_channel(adc1_handle, JOY_Y_PIN, &chan_config_y));
joystick_mutex = xSemaphoreCreateMutex();
while (1) {
int adc_raw_x = 0;
int adc_raw_y = 0;
// Read the joystick values
// Note: Check for ESP_OK error return here for production code
adc_oneshot_read(adc1_handle, JOY_X_PIN, &adc_raw_x);
adc_oneshot_read(adc1_handle, JOY_Y_PIN, &adc_raw_y);
// =======================================================
// ✨ NEW LINE ADDED HERE TO GENERATE LOG OUTPUT
// =======================================================
FreeOSLogI(TAG, "X-Axis Raw: %d | Y-Axis Raw: %d", adc_raw_x, adc_raw_y);
// =======================================================
// Safely update global variables
if (xSemaphoreTake(joystick_mutex, QUEUE_MAX_DELAY) == pdTRUE) {
g_joystick_x = adc_raw_x;
g_joystick_y = adc_raw_y;
xSemaphoreGive(joystick_mutex);
}
// Delay to prevent the task from consuming too much CPU time
vTaskDelay(pdMS_TO_TICKS(50)); // Read the joystick every 50ms
}
}
void initializeUIST7789(void) {
// 1. Initialize SPI Bus
const spi_bus_config_t bus_config = {
.mosi_io_num = PIN_NUM_MOSI,
.miso_io_num = -1,
.sclk_io_num = PIN_NUM_CLK,
.quadwp_io_num = -1,
.quadhd_io_num = -1,
.max_transfer_sz = 0, // Set to 0 to allow max size transfer
};
ESP_ERROR_CHECK(spi_bus_initialize(SPI2_HOST, &bus_config, SPI_DMA_CH_AUTO));
// 2. Initialize LCD Panel I/O (Interface to the ST7789)
const esp_lcd_panel_io_spi_config_t io_config = {
.cs_gpio_num = PIN_NUM_CS,
.dc_gpio_num = PIN_NUM_DC,
.spi_mode = 0,
.lcd_cmd_bits = 8,
.lcd_param_bits = 8,
.trans_queue_depth = 10,
.on_color_trans_done = NULL,
.pclk_hz = LCD_PIXEL_CLOCK_HZ, // Corrected clock field name
};
esp_lcd_panel_io_handle_t io_handle = NULL;
ESP_ERROR_CHECK(esp_lcd_new_panel_io_spi((esp_lcd_spi_bus_handle_t)SPI2_HOST, &io_config, &io_handle));
// 3. Initialize LCD Driver
const esp_lcd_panel_dev_config_t panel_config = {
.reset_gpio_num = PIN_NUM_RST,
.bits_per_pixel = 16,
.color_space = ESP_LCD_COLOR_SPACE_BGR,
};
const size_t ili9488_buffer_size = 0;
// Conditional compilation for the driver initialization
#if defined(USE_ILI9488)
ESP_ERROR_CHECK(esp_lcd_new_panel_ili9488(io_handle, &panel_config, ili9488_buffer_size, &panel_handle));
#elif defined(USE_ST7789)
// The ST7789 function does not take the buffer_size argument
ESP_ERROR_CHECK(esp_lcd_new_panel_st7789(io_handle, &panel_config, &panel_handle));
#endif
// 4. Panel Initialization (Reset and basic setup commands)
esp_lcd_panel_reset(panel_handle);
esp_lcd_panel_init(panel_handle);
esp_lcd_panel_swap_xy(panel_handle, true); // Often needed for orientation
esp_lcd_panel_set_gap(panel_handle, 0, 0);
#if defined(USE_ST7789)
esp_lcd_panel_invert_color(panel_handle, true);
#endif
esp_lcd_panel_disp_on_off(panel_handle, true);
#define LVGL_DISP_BUF_SIZE (LCD_H_RES * LCD_V_RES / 10) // E.g., 1/10th of screen
// 1. Initialize LVGL Core
const lvgl_port_cfg_t lvgl_cfg = ESP_LVGL_PORT_INIT_CONFIG();
// Use the port init function to set up the LVGL task and tick source
lvgl_port_init(&lvgl_cfg);
// 2. Register Display with LVGL Porting Layer
const lvgl_port_display_cfg_t disp_cfg = {
.io_handle = io_handle,
.panel_handle = panel_handle,
.control_handle = NULL, // Generally unused for basic SPI display
// --- Memory Configuration ---
// LVGL will allocate the buffer internally based on these fields.
.buffer_size = LVGL_DISP_BUF_SIZE,
.double_buffer = true, // We will allocate two buffers of the size above
.trans_size = 0, // Optional: Set to 0 if not using a dedicated transaction buffer
// --- Flags (Crucial for Byte Ordering) ---
.flags.buff_dma = 1,
// Set this flag to 1 (true) to swap the high and low bytes of the 16-bit color.
// This often resolves the "fuzziness" and color mixing issues on SPI displays.
#if defined(USE_ILI9488)
.flags.swap_bytes = 0,
#elif defined(USE_ST7789)
.flags.swap_bytes = 1,
#endif
// --- Display Resolution ---
.hres = LCD_V_RES,
.vres = LCD_H_RES,
.monochrome = false,
// --- LVGL v9 Configuration ---
.color_format = LV_COLOR_FORMAT_RGB565, // Use RGB565 for 16-bit color
// --- Flags (Crucial for DMA and memory location) ---
.flags.buff_dma = 1, // 1: Allocate buffers in DMA-capable memory (DRAM)
.flags.buff_spiram = 0, // 0: Do NOT allocate in PSRAM
.flags.sw_rotate = 0, // 0: Use hardware/driver rotation (faster)
.flags.full_refresh = 0, // 0: Use partial refresh (more efficient)
.flags.direct_mode = 0, // 0: Use standard buffered mode
.rotation = {0}, // Use default rotation (0 degrees)
};
// Use the registration function to wire it up
lv_display_t *disp = lvgl_port_add_disp(&disp_cfg);
// 3. Start your LVGL UI Task
xTaskCreate(lvgl_ui_task1, "LVGL_UI_Task", 8192, NULL, 5, NULL);
}
#include "driver/i2c_master.h" // <-- Use this new header
// Make sure these are defined correctly for your board
#define I2C_MASTER_SCL_IO PIN_NUM_I2C_SCL
#define I2C_MASTER_SDA_IO PIN_NUM_I2C_SDA
#define I2C_MASTER_NUM I2C_HOST // Use the same I2C port
/**
* @brief i2c master scanner (using new "next-gen" driver)
*/
static void i2c_scan_ng(void)
{
FreeOSLogI(TAG, "Starting I2C scan (NG Driver)...");
// 1. Create the bus configuration
i2c_master_bus_config_t bus_cfg = {
.i2c_port = I2C_MASTER_NUM,
.scl_io_num = I2C_MASTER_SCL_IO,
.sda_io_num = I2C_MASTER_SDA_IO,
.clk_source = I2C_CLK_SRC_DEFAULT,
.glitch_ignore_cnt = 7,
.flags.enable_internal_pullup = true,
};
i2c_master_bus_handle_t bus_handle;
ESP_ERROR_CHECK(i2c_new_master_bus(&bus_cfg, &bus_handle));
FreeOSLogI(TAG, "Scanning for I2C devices...");
for (uint8_t address = 1; address < 127; address++) {
// 2. Probe the address. This function sends the address and checks for an ACK.
// 50ms timeout is more than enough.
esp_err_t ret = i2c_master_probe(bus_handle, address, 50);
if (ret == ESP_OK) {
FreeOSLogI(TAG, "I2C device found at address 0x%02X", address);
} else if (ret != ESP_ERR_TIMEOUT && ret != ESP_ERR_NOT_FOUND) {
// Log other errors if they occur
ESP_LOGW(TAG, "Error 0x%X checking address 0x%02X", ret, address);
}
}
// 3. Clean up the bus
ESP_ERROR_CHECK(i2c_del_master_bus(bus_handle));
FreeOSLogI(TAG, "I2C scan complete.");
}
/**
* @brief GPIO Interrupt Service Routine
* This is called *every* time the INT_N_PIN goes LOW.
*/
static void IRAM_ATTR gpio_isr_handler(void *arg) {
uint32_t gpio_num = (uint32_t)arg;
if (gpio_num == INT_N_PIN) {
// Send an event to the touch task
// A non-zero value is posted to wake up the task
static uint32_t event = 1;
QueueSendFromISR(touch_intr_queue, &event, NULL);
}
}
/**
* @brief Task to process touch events
* This task waits for an event from the ISR queue.
*/
static void touch_task(void *arg) {
uint32_t event;
FT6336U_TouchPointType touch_data;
while (1) {
// Wait for an interrupt event from the queue
if (xQueueReceive(touch_intr_queue, &event, QUEUE_MAX_DELAY)) {
// Interrupt received, scan the touch controller
esp_err_t ret = ft6336u_scan(&touch_dev, &touch_data);
if (ret != ESP_OK) {
FreeOSLogE(TAG, "Failed to scan touch: %s", esp_err_to_name(ret));
continue;
}
// --- LVGL BRIDGE: UPDATE STATE ---
// Instead of logging, we now update the static variables
if (touch_data.touch_count == 0) {
g_last_state = LV_INDEV_STATE_REL; // Released
} else {
// Use the first touch point
g_last_x = touch_data.tp[0].x;
g_last_y = touch_data.tp[0].y;
g_last_state = LV_INDEV_STATE_PR; // Pressed
}
// --- END LVGL BRIDGE ---
if (touch_data.touch_count == 0) {
// FreeOSLogI(TAG, "Touch Released");
} else {
for (int i = 0; i < touch_data.touch_count; i++) {
FreeOSLogI(TAG, "Touch %d: (%s) X=%u, Y=%u",
i,
(touch_data.tp[i].status == touch) ? "NEW" : "STREAM",
touch_data.tp[i].x,
touch_data.tp[i].y);
}
}
}
}
}
/**
* @brief Initialize the I2C master bus
*/
/*esp_err_t i2c_master_init(void) {
i2c_master_bus_config_t i2c_bus_config = {
.i2c_port = I2C_HOST,
.sda_io_num = I2C_SDA_PIN,
.scl_io_num = I2C_SCL_PIN,
.clk_source = I2C_CLK_SRC_DEFAULT,
.glitch_ignore_cnt = 7, // Corrected: Was .glitch_ignore_cfg.flags.val = 0
.intr_priority = 0, // Use 0 for default priority
.flags.enable_internal_pullup = true // Enable internal pullups
// Removed non-existent fields:
// .scl_pulse_period_us
// .sda_pulse_period_us
// .intr_flags
};
esp_err_t ret = i2c_new_master_bus(&i2c_bus_config, &i2c_bus_handle);
if (ret != ESP_OK) {
FreeOSLogE(TAG, "Failed to create I2C master bus: %s", esp_err_to_name(ret));
}
return ret;
}
*/
/**
* @brief Initializes just the FT6336U chip
*/
esp_err_t ft6336u_driver_init(void) {
// 1. Initialize I2C Bus
ESP_ERROR_CHECK(i2c_master_init());
// 2. Initialize FT6336U Driver
ESP_ERROR_CHECK(ft6336u_init(&touch_dev, i2c_bus_handle, RST_N_PIN, INT_N_PIN));
touch_intr_queue = QueueCreate(10, sizeof(uint32_t));
FreeOSLogI(TAG, "Touch driver initialized successfully.");
// 5. Configure GPIO interrupt for INT_N_PIN
// We do this *after* the driver is init (which configures the pin)
// We re-configure to add the interrupt
gpio_config_t io_conf = {
.pin_bit_mask = (1ULL << INT_N_PIN),
.mode = GPIO_MODE_INPUT,
.pull_up_en = GPIO_PULLUP_ENABLE,
.intr_type = GPIO_INTR_NEGEDGE, // Trigger on FALLING edge
};
ESP_ERROR_CHECK(gpio_config(&io_conf));
// 6. Install GPIO ISR service and add handler
ESP_ERROR_CHECK(gpio_install_isr_service(0)); // 0 = default flags
ESP_ERROR_CHECK(gpio_isr_handler_add(INT_N_PIN, gpio_isr_handler, (void *)INT_N_PIN));
FreeOSLogI(TAG, "Interrupt handler installed. Ready for touch.");
return ESP_OK;
}
/* * This function should be called ONCE during your setup,
* right after lv_init() and after your display driver is registered.
*/
void lvgl_touch_driver_init(void) {
// 1. Create a new input device
lv_indev_t *indev_touchpad = lv_indev_create();
// 2. Set its type to "Pointer" (e.g., a mouse or touchscreen)
lv_indev_set_type(indev_touchpad, LV_INDEV_TYPE_POINTER);
// 3. THIS IS THE CRITICAL LINE:
// Register your C function (lvgl_touch_read_cb) as the
// official callback that LVGL will use to read input.
lv_indev_set_read_cb(indev_touchpad, lvgl_touch_read_cb);
}
void initTouch(void) {
FreeOSLogI(TAG, "Starting FT6336U Touch Example");
// 1. Initialize I2C Bus
ESP_ERROR_CHECK(i2c_master_init());
// 2. Initialize FT6336U Driver
// We pass the bus handle, and the driver adds itself as a device
ESP_ERROR_CHECK(ft6336u_init(&touch_dev, i2c_bus_handle, RST_N_PIN, INT_N_PIN));
FreeOSLogI(TAG, "Touch driver initialized successfully.");
// 3. Create the interrupt queue
touch_intr_queue = QueueCreate(10, sizeof(uint32_t));
// 4. Create the touch processing task
xTaskCreate(touch_task, "touch_task", 4096, NULL, 5, NULL);
// 5. Configure GPIO interrupt for INT_N_PIN
// We do this *after* the driver is init (which configures the pin)
// We re-configure to add the interrupt
gpio_config_t io_conf = {
.pin_bit_mask = (1ULL << INT_N_PIN),
.mode = GPIO_MODE_INPUT,
.pull_up_en = GPIO_PULLUP_ENABLE,
.intr_type = GPIO_INTR_NEGEDGE, // Trigger on FALLING edge
};
ESP_ERROR_CHECK(gpio_config(&io_conf));
// 6. Install GPIO ISR service and add handler
ESP_ERROR_CHECK(gpio_install_isr_service(0)); // 0 = default flags
ESP_ERROR_CHECK(gpio_isr_handler_add(INT_N_PIN, gpio_isr_handler, (void *)INT_N_PIN));
FreeOSLogI(TAG, "Interrupt handler installed. Ready for touch.");
}
// --- PIN CONFIGURATION ---
#define I2S_BCK_IO GPIO_NUM_39
#define I2S_WS_IO GPIO_NUM_40 // Also known as LRCK
#define I2S_DO_IO GPIO_NUM_41 // Also known as DIN
void i2s_task(void *args)
{
// 1. BUFFER SETUP
// Create a buffer for stereo samples (Left + Right)
// 16-bit depth * 2 channels * 1024 samples
size_t buffer_len = 1024;
int16_t *samples = (int16_t *)calloc(buffer_len, 2 * sizeof(int16_t));
// Fill buffer with a Sine Wave
// We pre-calculate one buffer and play it repeatedly for this test
for (int i = 0; i < buffer_len; i++) {
float t = (float)i / SAMPLE_RATE;
int16_t val = (int16_t)(3000.0f * sinf(2.0f * PI * WAVE_FREQ_HZ * t));
// Interleaved Stereo: [Left, Right, Left, Right...]
samples[i * 2] = val; // Left Channel
samples[i * 2 + 1] = val; // Right Channel
}
size_t bytes_written = 0;
while (1) {
// Write the buffer to the I2S peripheral
// This function blocks until the DMA buffer has space
i2s_channel_write(tx_handle, samples, buffer_len * 2 * sizeof(int16_t), &bytes_written, QUEUE_MAX_DELAY);
}
}
void createI2STask() {
// 2. CHANNEL CONFIGURATION
i2s_chan_config_t chan_cfg = I2S_CHANNEL_DEFAULT_CONFIG(I2S_NUM_AUTO, I2S_ROLE_MASTER);
ESP_ERROR_CHECK(i2s_new_channel(&chan_cfg, &tx_handle, NULL));
// 3. STANDARD I2S MODE CONFIGURATION (Philips Format)
i2s_std_config_t std_cfg = {
.clk_cfg = I2S_STD_CLK_DEFAULT_CONFIG(SAMPLE_RATE),
.slot_cfg = I2S_STD_PHILIPS_SLOT_DEFAULT_CONFIG(I2S_DATA_BIT_WIDTH_16BIT, I2S_SLOT_MODE_STEREO),
.gpio_cfg = {
.mclk = I2S_GPIO_UNUSED, // We are grounding SCK on the PCM5102, so MCLK is not needed
.bclk = I2S_BCK_IO,
.ws = I2S_WS_IO,
.dout = I2S_DO_IO,
.din = I2S_GPIO_UNUSED, // We are only outputting audio
.invert_flags = {
.mclk_inv = false,
.bclk_inv = false,
.ws_inv = false,
},
},
};
// 4. INITIALIZE AND ENABLE
ESP_ERROR_CHECK(i2s_channel_init_std_mode(tx_handle, &std_cfg));
ESP_ERROR_CHECK(i2s_channel_enable(tx_handle));
printf("I2S initialized on BCK:%d, LRCK:%d, DIN:%d\n", I2S_BCK_IO, I2S_WS_IO, I2S_DO_IO);
// 5. START TASK
xTaskCreate(i2s_task, "i2s_task", 4096, NULL, 5, NULL);
}
// Define your pins here
#define PIN_NUM_CLK 14
#define PIN_NUM_CMD 15
#define PIN_NUM_D0 16
#define PIN_NUM_D1 17
#define PIN_NUM_D2 18
#define PIN_NUM_D3 13
/*
// --- 2. SD Card (SDIO 4-bit) Pin Definitions ---
#define SDIO_PIN_NUM_CLK 5
#define SDIO_PIN_NUM_CMD 6
#define SDIO_PIN_NUM_D0 7
#define SDIO_PIN_NUM_D1 8
#define SDIO_PIN_NUM_D2 9
#define SDIO_PIN_NUM_D3 10
*/
void mount_sd_card()
{
esp_err_t ret;
// Options for mounting the filesystem.
// If format_if_mount_failed is set to true, SD card will be partitioned and
// formatted in case when mounting fails.
esp_vfs_fat_sdmmc_mount_config_t mount_config = {
.format_if_mount_failed = false,
.max_files = 5,
.allocation_unit_size = 16 * 1024
};
// Use settings defined above to initialize SD card and mount FATFS.
// Note: esp_vfs_fat_sdmmc_mount is an all-in-one convenience function.
// Please check its source code and implement error recovery when developing production applications.
sdmmc_card_t *card;
const char mount_point[] = "/sdcard";
// By default, SDMMC host frequency is set to 20MHz.
// If your wires are long (>10cm), consider lowering this to 10MHz or 400kHz.
sdmmc_host_t host = SDMMC_HOST_DEFAULT();
host.max_freq_khz = SDMMC_FREQ_DEFAULT;
//host.max_freq_khz = 100;
// This is where we configure the pins
sdmmc_slot_config_t slot_config = SDMMC_SLOT_CONFIG_DEFAULT();
//slot_config.width = 4; // Using 4-bit mode
slot_config.width = 1; // Using 4-bit mode
// MAPPING THE PINS
slot_config.clk = (gpio_num_t)PIN_NUM_CLK;
slot_config.cmd = (gpio_num_t)PIN_NUM_CMD;
slot_config.d0 = (gpio_num_t)PIN_NUM_D0;
slot_config.d1 = (gpio_num_t)PIN_NUM_D1;
slot_config.d2 = (gpio_num_t)PIN_NUM_D2;
slot_config.d3 = (gpio_num_t)PIN_NUM_D3;
slot_config.gpio_cd = GPIO_NUM_21;
// CRITICAL: Enable internal pull-ups if your breakout board doesn't have them
slot_config.flags |= SDMMC_SLOT_FLAG_INTERNAL_PULLUP;
printf("Mounting filesystem...\n");
ret = esp_vfs_fat_sdmmc_mount(mount_point, &host, &slot_config, &mount_config, &card);
if (ret != ESP_OK) {
if (ret == ESP_FAIL) {
printf("Failed to mount filesystem. \n");
} else {
printf("Failed to initialize the card (%s). \n", esp_err_to_name(ret));
}
return;
}
printf("Filesystem mounted\n");
sdmmc_card_print_info(stdout, card);
}
static i2c_master_bus_handle_t audio_i2c_bus_handle;
//#include "driver/i2c_master.h"
//#include "esp_log.h"
void check_pins_1_and_2(void) {
const char *TAG = "I2C_CHECK";
ESP_LOGW(TAG, "--- DIAGNOSTIC: Checking Pins 1 (SCL) and 2 (SDA) ---");
// 1. Configure the Bus specifically for these pins
i2c_master_bus_config_t conf = {
.clk_source = I2C_CLK_SRC_DEFAULT,
.i2c_port = -1, // Auto-select port
.scl_io_num = 1, // <--- Pin 1
.sda_io_num = 2, // <--- Pin 2
.glitch_ignore_cnt = 7,
.flags.enable_internal_pullup = true, // Attempt to use internal pullups
};
i2c_master_bus_handle_t bus_handle;
esp_err_t err = i2c_new_master_bus(&conf, &bus_handle);
if (err != ESP_OK) {
FreeOSLogE(TAG, "CRITICAL: Could not init I2C on Pins 1 & 2. Error: %s", esp_err_to_name(err));
return;
}
// 2. Run the Scan
int devices_found = 0;
for (uint8_t addr = 0x01; addr < 0x7F; addr++) {
// Probe every address
if (i2c_master_probe(bus_handle, addr, 50) == ESP_OK) {
FreeOSLogI(TAG, " -> SUCCESS: Found Device at 0x%02X", addr);
devices_found++;
}
}
// 3. Report Results
if (devices_found == 0) {
FreeOSLogE(TAG, "FAILURE: No devices found. Pins 1 & 2 are dead or wired incorrectly.");
} else {
FreeOSLogI(TAG, "PASSED: Found %d device(s) on Pins 1 & 2.", devices_found);
}
// 4. Clean up (Release pins so your real code can use them)
i2c_del_master_bus(bus_handle);
ESP_LOGW(TAG, "--- DIAGNOSTIC COMPLETE ---");
}
// Add these defines if not already in your file
#define ES8311_ADDR 0x18
void initEs8311(void) {
FreeOSLogI("AUDIO", "Initializing ES8311...");
// 1. Create the I2C Bus Handle (Standard for IDF 5.x)
// We recreate the bus here because the diagnostic tool closed its temporary one.
i2c_master_bus_config_t i2c_bus_config = {
.i2c_port = -1,
.sda_io_num = 2, // GPIO 2
.scl_io_num = 1, // GPIO 1
.clk_source = I2C_CLK_SRC_DEFAULT,
.glitch_ignore_cnt = 7,
};
// Create the global handle (make sure 'audio_i2c_bus_handle' is defined at top of file)
ESP_ERROR_CHECK(i2c_new_master_bus(&i2c_bus_config, &audio_i2c_bus_handle));
// 2. Create the Device Handle
es8311_handle_t es_handle = es8311_create(audio_i2c_bus_handle, ES8311_ADDR);
// 3. Configure Clock (CRITICAL FIX: frequency MUST be set)
es8311_clock_config_t clk_cfg = {
.mclk_from_mclk_pin = true,
.sample_frequency = 44100,
.mclk_frequency = 11289600 // 44100 * 256
};
ESP_ERROR_CHECK(es8311_init(es_handle, &clk_cfg, ES8311_RESOLUTION_16, ES8311_RESOLUTION_16));
// 4. --- THE SILENCE FIXES ---
// A. Force Mic Bias ON (Powers the microphone element)
uint8_t reg14;
es8311_read_reg(es_handle, 0x14, ®14);
reg14 |= (1 << 4);
es8311_write_reg(es_handle, 0x14, reg14);
// B. Force Max Gain (So we can hear even faint noise)
es8311_write_reg(es_handle, 0x16, 0xBB); // Analog Gain (+30dB)
es8311_write_reg(es_handle, 0x17, 0xFF); // Digital Volume (0dB / Max)
// C. Select the Correct Input Pin (THE USUAL SUSPECT)
// Most modules use Input 1 (0x00). Some use Input 2 (0x50).
// We will try Input 1 first. If silent, change this to 0x50.
//es8311_write_reg(es_handle, 0x44, 0x00);
// C. Select Input 2 (Common for breakout boards)
// Change 0x00 to 0x50
es8311_write_reg(es_handle, 0x44, 0x50);
// D. Force ADC Unmute (Register 0x15)
// Bit 6 controls mute. We must clear it to 0.
uint8_t reg15;
es8311_read_reg(es_handle, 0x15, ®15);
reg15 &= ~(1 << 6); // Clear Bit 6
es8311_write_reg(es_handle, 0x15, reg15);
// D. Unmute Everything
es8311_voice_mute(es_handle, false);
es8311_microphone_config(es_handle, false); // False = Analog Mic
FreeOSLogI("AUDIO", "ES8311 Init Complete. Gain set to MAX.");
}
void debug_microphone_task(void *args) {
ESP_LOGW("MIC_DEBUG", "Starting Microphone Monitor... (Check Serial Plotter!)");
size_t chunk_size = 1024; // Bytes
int16_t *buffer = cc_safe_alloc(1, chunk_size);
size_t bytes_read = 0;
while (1) {
// 1. Read Raw Data from I2S
// Ensure 'rx_handle' is the global handle you created in setupHybridAudio
if (i2s_channel_read(rx_handle, buffer, chunk_size, &bytes_read, 1000) == ESP_OK) {
// 2. Process the Data (Find Peak Volume)
int32_t sum_left = 0;
int32_t max_left = 0;
int32_t max_right = 0;
int samples = bytes_read / 2; // Total 16-bit samples
// Iterate by 2 because we are in Stereo (Left, Right, Left, Right...)
for (int i = 0; i < samples; i += 2) {
int16_t left_val = buffer[i];
int16_t right_val = buffer[i+1];
if (abs(left_val) > max_left) max_left = abs(left_val);
if (abs(right_val) > max_right) max_right = abs(right_val);
}
// 3. Print Results (Use Serial Plotter for cool graphs)
// If these numbers are > 50, your mic is working.
// If these numbers are 0, your mic is dead.
FreeOSLogI("MIC_DEBUG", "Left Level: %ld | Right Level: %ld", (long)max_left, (long)max_right);
} else {
FreeOSLogE("MIC_DEBUG", "I2S Read Failed!");
}
// Slow down the logs so you can read them
vTaskDelay(pdMS_TO_TICKS(100));
}
free(buffer);
vTaskDelete(NULL);
}
void initializeI2S(void) {
// --- STEP 1: INITIALIZE I2S (The "Road") ---
i2s_chan_config_t chan_cfg = I2S_CHANNEL_DEFAULT_CONFIG(I2S_NUM_AUTO, I2S_ROLE_MASTER);
// DMA Buffer Tuning for 16-bit
// 16-bit stereo frame = 4 bytes.
// 1024 frames * 4 bytes = 4096 bytes (This fits in the DMA limit!)
chan_cfg.dma_desc_num = 8;
chan_cfg.dma_frame_num = 1023;
tx_handle = NULL;
ESP_ERROR_CHECK(i2s_new_channel(&chan_cfg, &tx_handle, &rx_handle));
i2s_std_config_t std_cfg = {
.clk_cfg = I2S_STD_CLK_DEFAULT_CONFIG(44100),
// --- CHANGE: Set to 16-bit ---
.slot_cfg = I2S_STD_PHILIPS_SLOT_DEFAULT_CONFIG(I2S_DATA_BIT_WIDTH_16BIT, I2S_SLOT_MODE_STEREO),
.gpio_cfg = {
.mclk = GPIO_NUM_42,
.bclk = GPIO_NUM_39,
.ws = GPIO_NUM_40,
.dout = GPIO_NUM_41,
.din = GPIO_NUM_38,
},
};
ESP_ERROR_CHECK(i2s_channel_init_std_mode(tx_handle, &std_cfg));
ESP_ERROR_CHECK(i2s_channel_init_std_mode(rx_handle, &std_cfg));
ESP_ERROR_CHECK(i2s_channel_enable(tx_handle));
ESP_ERROR_CHECK(i2s_channel_enable(rx_handle));
}
void testGraphics(void) {
}
// --- Configuration ---
#define LEDC_TIMER LEDC_TIMER_0
#define LEDC_MODE LEDC_LOW_SPEED_MODE // S3 only uses Low Speed Mode
#define LEDC_OUTPUT_IO (47) // Your requested Pin
#define LEDC_CHANNEL LEDC_CHANNEL_0
#define LEDC_DUTY_RES LEDC_TIMER_13_BIT // 8192 discrete levels of brightness
#define LEDC_FREQUENCY (4000) // 4kHz (Safe for CN5711)
// --- Initialization Function ---
void example_ledc_init(void)
{
// 1. Prepare and then apply the LEDC PWM timer configuration
ledc_timer_config_t ledc_timer = {
.speed_mode = LEDC_MODE,
.timer_num = LEDC_TIMER,
.duty_resolution = LEDC_DUTY_RES,
.freq_hz = LEDC_FREQUENCY, // Set frequency of PWM signal
.clk_cfg = LEDC_AUTO_CLK
};
ESP_ERROR_CHECK(ledc_timer_config(&ledc_timer));
// 2. Prepare and then apply the LEDC PWM channel configuration
ledc_channel_config_t ledc_channel = {
.speed_mode = LEDC_MODE,
.channel = LEDC_CHANNEL,
.timer_sel = LEDC_TIMER,
.intr_type = LEDC_INTR_DISABLE,
.gpio_num = LEDC_OUTPUT_IO,
.duty = 0, // Set duty to 0% initially
.hpoint = 0
};
ESP_ERROR_CHECK(ledc_channel_config(&ledc_channel));
}
// --- The Dedicated Task ---
void pwm_fade_task(void *pvParameters)
{
uint32_t max_duty = 8191; // 2^13 - 1
int fade_step = 100; // How much to jump per cycle
int delay_ms = 10; // Speed of the fade
FreeOSLogI(TAG, "Starting PWM Fade Loop...");
while (1) {
// Fade UP
for (int duty = 0; duty <= max_duty; duty += fade_step) {
ledc_set_duty(LEDC_MODE, LEDC_CHANNEL, duty);
ledc_update_duty(LEDC_MODE, LEDC_CHANNEL);
vTaskDelay(pdMS_TO_TICKS(delay_ms));
}
// Fade DOWN
for (int duty = max_duty; duty >= 0; duty -= fade_step) {
ledc_set_duty(LEDC_MODE, LEDC_CHANNEL, duty);
ledc_update_duty(LEDC_MODE, LEDC_CHANNEL);
vTaskDelay(pdMS_TO_TICKS(delay_ms));
}
// Wait at bottom (Off) for 1 second
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
// At top of file
volatile float g_battery_level = 0.0f;
// --- CONFIGURATION ---
#define ADC_UNIT ADC_UNIT_2
#define ADC_CHANNEL ADC_CHANNEL_7 // GPIO 34 on original ESP32
#define ADC_ATTEN ADC_ATTEN_DB_12
#define VOLTAGE_DIVIDER 2.0f
static const char *TAGBattery = "BATTERY_TASK";
// Represents a point on the battery curve: {Voltage, Percentage}
typedef struct {
float voltage;
int percentage;
} BatteryPoint;
// Standard Li-Ion Discharge Curve (approximate)
// You can tweak these voltages based on your real-world testing
static const BatteryPoint curve[] = {
{4.20, 100},
{4.10, 90},
{4.00, 80},
{3.90, 70},
{3.80, 60},
{3.70, 50}, // Nominal voltage is usually around 50%
{3.65, 40},
{3.60, 30}, // Drops quickly after this
{3.50, 20},
{3.40, 10},
{3.20, 0} // Cutoff
};
#define POINTS_COUNT (sizeof(curve) / sizeof(curve[0]))
uint8_t get_battery_percentage(float voltage)
{
// 1. Clamp extremely high/low values
if (voltage >= curve[0].voltage) return 100;
if (voltage <= curve[POINTS_COUNT - 1].voltage) return 0;
// 2. Loop through the table to find the range we are in
for (int i = 0; i < POINTS_COUNT - 1; i++) {
// Find the two points sandwiching our current voltage
if (voltage <= curve[i].voltage && voltage > curve[i + 1].voltage) {
float v1 = curve[i].voltage;
float v2 = curve[i + 1].voltage;
int p1 = curve[i].percentage;
int p2 = curve[i + 1].percentage;
// 3. Linear Interpolation (Math to smooth the gap)
// This prevents the percentage from jumping 60% -> 50% instantly.
// It will give you 58%, 57%, etc.
float percent = p1 + ((voltage - v1) * (p2 - p1) / (v2 - v1));
return (uint8_t)percent;
}
}
return 0; // Should not reach here
}
// 1. The Task Function
// This contains the setup AND the infinite loop for this specific job
void battery_monitor_task(void *pvParameters)
{
// --- SETUP PHASE (Runs once) ---
// 1. Init ADC Unit (Same as before)
adc_oneshot_unit_handle_t adc_handle;
adc_oneshot_unit_init_cfg_t init_config = {
.unit_id = ADC_UNIT,
};
ESP_ERROR_CHECK(adc_oneshot_new_unit(&init_config, &adc_handle));
// 2. Configure Channel (Same as before)
adc_oneshot_chan_cfg_t config = {
.bitwidth = ADC_BITWIDTH_DEFAULT,
.atten = ADC_ATTEN,
};
ESP_ERROR_CHECK(adc_oneshot_config_channel(adc_handle, ADC_CHANNEL, &config));
// 3. Setup Calibration (CHANGED FOR S3)
adc_cali_handle_t adc_cali_handle = NULL;
// S3 uses "Curve Fitting", not "Line Fitting"
adc_cali_curve_fitting_config_t cali_config = {
.unit_id = ADC_UNIT,
.atten = ADC_ATTEN,
.bitwidth = ADC_BITWIDTH_DEFAULT,
};
bool do_calibration = false;
// Note: Function name is also different (create_scheme_curve_fitting)
if (adc_cali_create_scheme_curve_fitting(&cali_config, &adc_cali_handle) == ESP_OK) {
do_calibration = true;
FreeOSLogI(TAG, "ADC Calibration Initialized");
} else {
ESP_LOGW(TAG, "ADC Calibration Failed");
}
// --- LOOP PHASE (Runs forever) ---
while (1) {
//int adc_raw = 0;
int voltage_mv_pin = 0;
float battery_voltage = 0;
// Read Raw
//ESP_ERROR_CHECK(adc_oneshot_read(adc_handle, ADC_CHANNEL, &adc_raw));
/*if (do_calibration) {
// Convert to Millivolts
ESP_ERROR_CHECK(adc_cali_raw_to_voltage(adc_cali_handle, adc_raw, &voltage_mv_pin));
// Apply Divider Math
battery_voltage = (voltage_mv_pin * VOLTAGE_DIVIDER) / 1000.0f;
FreeOSLogI(TAGBattery, "Battery: %.2f V (%d mV)", battery_voltage, voltage_mv_pin);
} else {
ESP_LOGW(TAGBattery, "Raw: %d (No Calib)", adc_raw);
}*/
uint32_t raw_sum = 0;
const int SAMPLES = 64; // Take 64 samples to smooth out noise
// 1. Burst Read
for (int i = 0; i < SAMPLES; i++) {
int temp_raw = 0;
adc_oneshot_read(adc_handle, ADC_CHANNEL, &temp_raw);
raw_sum += temp_raw;
// Tiny delay helps separate noise spikes
vTaskDelay(pdMS_TO_TICKS(100));
}
// 2. Average
int adc_raw = raw_sum / SAMPLES;
if (do_calibration) {
// Convert the AVERAGED raw value to voltage
ESP_ERROR_CHECK(adc_cali_raw_to_voltage(adc_cali_handle, adc_raw, &voltage_mv_pin));
battery_voltage = (voltage_mv_pin * 2.0f) / 1000.0f;
uint8_t pct = get_battery_percentage(battery_voltage);
FreeOSLogI(TAG, "Battery (Avg): %.2f V | %d%%", battery_voltage, pct);
}
// Inside Task loop
g_battery_level = battery_voltage;
// Inside any other task
printf("Current Level: %.2f", g_battery_level);
// 3. Convert to Percentage
uint8_t pct = get_battery_percentage(battery_voltage);
FreeOSLogI(TAG, "Battery: %.2f V | %d%%", battery_voltage, pct);
// Delay for 5 seconds (5000ms)
// This frees up the CPU for other tasks
vTaskDelay(pdMS_TO_TICKS(100));
}
// Tasks should never return. If they do, they must delete themselves.
vTaskDelete(NULL);
}