ESP BOX
Esp-Box
The ESP32-S3-BOX and ESP32-S3-BOX-3 are development boards for the ESP32-S3 module. They feature a nice touchscreen display, a speaker, microphones, and expansion headers.
The espp::EspBox component provides a singleton hardware abstraction for initializing the touch, display, and audio subsystems, as well as automatically determining which version of the Box it’s running on.
API Reference
Header File
Classes
-
class EspBox : public espp::BaseComponent
The EspBox class provides an interface to the ESP32-S3-BOX and ESP32-S3-BOX-3 development boards.
The class provides access to the following features:
Touchpad
Audio
Interrupts
Buttons (boot and mute)
I2C
IMU (Inertial Measurement Unit), 6-axis ICM42607
The class is a singleton and can be accessed using the get() method.
Example
espp::EspBox &box = espp::EspBox::get(); box.set_log_level(espp::Logger::Verbosity::INFO); logger.info("Running on {}", box.box_type()); auto touch_callback = [&](const auto &touch) { // NOTE: since we're directly using the touchpad data, and not using the // TouchpadInput + LVGL, we'll need to ensure the touchpad data is // converted into proper screen coordinates instead of simply using the // raw values. static auto previous_touchpad_data = box.touchpad_convert(touch); auto touchpad_data = box.touchpad_convert(touch); if (touchpad_data != previous_touchpad_data) { logger.info("Touch: {}", touchpad_data); previous_touchpad_data = touchpad_data; // if the button is pressed, clear the circles if (touchpad_data.btn_state) { std::lock_guard<std::recursive_mutex> lock(lvgl_mutex); clear_circles(); } // if there is a touch point, draw a circle and play a click sound if (touchpad_data.num_touch_points > 0) { play_click(box); std::lock_guard<std::recursive_mutex> lock(lvgl_mutex); draw_circle(touchpad_data.x, touchpad_data.y, 10); } } }; // initialize the sound if (!box.initialize_sound()) { logger.error("Failed to initialize sound!"); return; } // initialize the LCD if (!box.initialize_lcd()) { logger.error("Failed to initialize LCD!"); return; } // set the pixel buffer to be 50 lines high static constexpr size_t pixel_buffer_size = box.lcd_width() * 50; // initialize the LVGL display for the esp-box if (!box.initialize_display(pixel_buffer_size)) { logger.error("Failed to initialize display!"); return; } // initialize the touchpad if (!box.initialize_touch(touch_callback)) { logger.error("Failed to initialize touchpad!"); return; } // make the filter we'll use for the IMU to compute the orientation static constexpr float angle_noise = 0.001f; static constexpr float rate_noise = 0.1f; static espp::KalmanFilter<2> kf; kf.set_process_noise(rate_noise); kf.set_measurement_noise(angle_noise); static constexpr float beta = 0.1f; // higher = more accelerometer, lower = more gyro static espp::MadgwickFilter f(beta); using Imu = espp::EspBox::Imu; auto kalman_filter_fn = [](float dt, const Imu::Value &accel, const Imu::Value &gyro) -> Imu::Value { // Apply Kalman filter float accelRoll = atan2(accel.y, accel.z); float accelPitch = atan2(-accel.x, sqrt(accel.y * accel.y + accel.z * accel.z)); kf.predict({espp::deg_to_rad(gyro.x), espp::deg_to_rad(gyro.y)}, dt); kf.update({accelRoll, accelPitch}); float roll, pitch; std::tie(roll, pitch) = kf.get_state(); // return the computed orientation Imu::Value orientation{}; orientation.roll = roll; orientation.pitch = pitch; orientation.yaw = 0.0f; return orientation; }; auto madgwick_filter_fn = [](float dt, const Imu::Value &accel, const Imu::Value &gyro) -> Imu::Value { // Apply Madgwick filter f.update(dt, accel.x, accel.y, accel.z, espp::deg_to_rad(gyro.x), espp::deg_to_rad(gyro.y), espp::deg_to_rad(gyro.z)); float roll, pitch, yaw; f.get_euler(roll, pitch, yaw); // return the computed orientation Imu::Value orientation{}; orientation.roll = espp::deg_to_rad(roll); orientation.pitch = espp::deg_to_rad(pitch); orientation.yaw = espp::deg_to_rad(yaw); return orientation; }; // initialize the IMU if (!box.initialize_imu(kalman_filter_fn)) { logger.error("Failed to initialize IMU!"); return; } // set the background color to black lv_obj_t *bg = lv_obj_create(lv_screen_active()); lv_obj_set_size(bg, box.lcd_width(), box.lcd_height()); lv_obj_set_style_bg_color(bg, lv_color_make(0, 0, 0), 0); // add text in the center of the screen lv_obj_t *label = lv_label_create(lv_screen_active()); static std::string label_text = "\n\n\n\nTouch the screen!\nPress the home button to clear circles."; lv_label_set_text(label, label_text.c_str()); lv_obj_align(label, LV_ALIGN_TOP_LEFT, 0, 0); lv_obj_set_style_text_align(label, LV_TEXT_ALIGN_LEFT, 0); /*Create style*/ static lv_style_t style_line0; lv_style_init(&style_line0); lv_style_set_line_width(&style_line0, 8); lv_style_set_line_color(&style_line0, lv_palette_main(LV_PALETTE_BLUE)); lv_style_set_line_rounded(&style_line0, true); // make a line for showing the direction of "down" lv_obj_t *line0 = lv_line_create(lv_screen_active()); static lv_point_precise_t line_points0[] = {{0, 0}, {box.lcd_width(), box.lcd_height()}}; lv_line_set_points(line0, line_points0, 2); lv_obj_add_style(line0, &style_line0, 0); /*Create style*/ static lv_style_t style_line1; lv_style_init(&style_line1); lv_style_set_line_width(&style_line1, 8); lv_style_set_line_color(&style_line1, lv_palette_main(LV_PALETTE_RED)); lv_style_set_line_rounded(&style_line1, true); // make a line for showing the direction of "down" lv_obj_t *line1 = lv_line_create(lv_screen_active()); static lv_point_precise_t line_points1[] = {{0, 0}, {box.lcd_width(), box.lcd_height()}}; lv_line_set_points(line1, line_points1, 2); lv_obj_add_style(line1, &style_line1, 0); // add a button in the top left which (when pressed) will rotate the display // through 0, 90, 180, 270 degrees lv_obj_t *btn = lv_btn_create(lv_screen_active()); lv_obj_set_size(btn, 50, 50); lv_obj_align(btn, LV_ALIGN_TOP_LEFT, 0, 0); lv_obj_t *label_btn = lv_label_create(btn); lv_label_set_text(label_btn, LV_SYMBOL_REFRESH); // center the text in the button lv_obj_align(label_btn, LV_ALIGN_CENTER, 0, 0); lv_obj_add_event_cb( btn, [](auto event) { std::lock_guard<std::recursive_mutex> lock(lvgl_mutex); clear_circles(); static auto rotation = LV_DISPLAY_ROTATION_0; rotation = static_cast<lv_display_rotation_t>((static_cast<int>(rotation) + 1) % 4); lv_display_t *disp = lv_display_get_default(); lv_disp_set_rotation(disp, rotation); }, LV_EVENT_PRESSED, nullptr); // disable scrolling on the screen (so that it doesn't behave weirdly when // rotated and drawing with your finger) lv_obj_set_scrollbar_mode(lv_screen_active(), LV_SCROLLBAR_MODE_OFF); lv_obj_clear_flag(lv_screen_active(), LV_OBJ_FLAG_SCROLLABLE); // start a simple thread to do the lv_task_handler every 16ms espp::Task lv_task({.callback = [](std::mutex &m, std::condition_variable &cv) -> bool { { std::lock_guard<std::recursive_mutex> lock(lvgl_mutex); lv_task_handler(); } std::unique_lock<std::mutex> lock(m); cv.wait_for(lock, 16ms); return false; }, .task_config = { .name = "lv_task", .stack_size_bytes = 6 * 1024, }}); lv_task.start(); // load the audio file (wav file bundled in memory) size_t wav_size = load_audio(); logger.info("Loaded {} bytes of audio", wav_size); // unmute the audio and set the volume to 60% box.mute(false); box.volume(60.0f); // set the display brightness to be 75% box.brightness(75.0f); // make a task to read out the IMU data and print it to console espp::Task imu_task( {.callback = [&](std::mutex &m, std::condition_variable &cv) -> bool { // sleep first in case we don't get IMU data and need to exit early { std::unique_lock<std::mutex> lock(m); cv.wait_for(lock, 10ms); } static auto &box = espp::EspBox::get(); static auto imu = box.imu(); auto now = esp_timer_get_time(); // time in microseconds static auto t0 = now; auto t1 = now; float dt = (t1 - t0) / 1'000'000.0f; // convert us to s t0 = t1; std::error_code ec; // update the imu data if (!imu->update(dt, ec)) { return false; } // get accel auto accel = imu->get_accelerometer(); auto gyro = imu->get_gyroscope(); auto temp = imu->get_temperature(); auto orientation = imu->get_orientation(); auto gravity_vector = imu->get_gravity_vector(); std::string text = fmt::format("{}\n\n\n\n\n", label_text); text += fmt::format("Accel: {:02.2f} {:02.2f} {:02.2f}\n", accel.x, accel.y, accel.z); text += fmt::format("Gyro: {:03.2f} {:03.2f} {:03.2f}\n", espp::deg_to_rad(gyro.x), espp::deg_to_rad(gyro.y), espp::deg_to_rad(gyro.z)); text += fmt::format("Angle: {:03.2f} {:03.2f}\n", espp::rad_to_deg(orientation.roll), espp::rad_to_deg(orientation.pitch)); text += fmt::format("Temp: {:02.1f} C\n", temp); // use the pitch to to draw a line on the screen indiating the // direction from the center of the screen to "down" int x0 = box.lcd_width() / 2; int y0 = box.lcd_height() / 2; int x1 = x0 + 50 * gravity_vector.x; int y1 = y0 + 50 * gravity_vector.y; static lv_point_precise_t line_points0[] = {{x0, y0}, {x1, y1}}; line_points0[1].x = x1; line_points0[1].y = y1; // Now show the madgwick filter auto madgwick_orientation = madgwick_filter_fn(dt, accel, gyro); float roll = madgwick_orientation.roll; float pitch = madgwick_orientation.pitch; [[maybe_unused]] float yaw = madgwick_orientation.yaw; float vx = sin(pitch); float vy = -cos(pitch) * sin(roll); [[maybe_unused]] float vz = -cos(pitch) * cos(roll); x1 = x0 + 50 * vx; y1 = y0 + 50 * vy; static lv_point_precise_t line_points1[] = {{x0, y0}, {x1, y1}}; line_points1[1].x = x1; line_points1[1].y = y1; std::lock_guard<std::recursive_mutex> lock(lvgl_mutex); lv_label_set_text(label, text.c_str()); lv_line_set_points(line0, line_points0, 2); lv_line_set_points(line1, line_points1, 2); return false; }, .task_config = { .name = "IMU", .stack_size_bytes = 6 * 1024, .priority = 10, .core_id = 0, }}); imu_task.start(); // loop forever while (true) { std::this_thread::sleep_for(1s); }
Public Types
-
enum class BoxType
The type of the box.
Values:
-
enumerator UNKNOWN
unknown box
-
enumerator BOX
ESP32-S3-BOX.
-
enumerator BOX3
ESP32-S3-BOX-3.
-
enumerator UNKNOWN
-
using button_callback_t = espp::Interrupt::event_callback_fn
Alias for the button callback function.
-
using Pixel = lv_color16_t
Alias for the pixel type used by the ESP-Box display.
-
using TouchpadData = espp::TouchpadData
Alias for the touchpad data used by the ESP-Box touchpad.
Public Functions
-
inline BoxType box_type() const
Get the type of the box
See also
- Returns
The type of the box that was detected
-
inline I2c &internal_i2c()
Get a reference to the internal I2C bus
Note
The internal I2C bus is used for the touchscreen and audio codec
- Returns
A reference to the internal I2C bus
-
inline espp::Interrupt &interrupts()
Get a reference to the interrupts
- Returns
A reference to the interrupts
-
bool initialize_touch(const touch_callback_t &callback = nullptr)
Initialize the touchpad
Note
This will configure the touchpad interrupt pin which will automatically call the touch callback function when the touchpad is touched
Warning
This method should be called after the display has been initialized if you want the touchpad to be recognized and used with LVGL and its objects.
- Parameters
callback – The touchpad callback
- Returns
true if the touchpad was successfully initialized, false otherwise
-
inline std::shared_ptr<TouchpadInput> touchpad_input() const
Get the touchpad input
- Returns
A shared pointer to the touchpad input
-
inline TouchpadData touchpad_data() const
Get the most recent touchpad data
- Returns
The touchpad data
-
void touchpad_read(uint8_t *num_touch_points, uint16_t *x, uint16_t *y, uint8_t *btn_state)
Get the most recent touchpad data
See also
Note
This method is a convenience method for integrating with LVGL, the data it returns is identical to the data returned by the touchpad_data() method
- Parameters
num_touch_points – The number of touch points
x – The x coordinate
y – The y coordinate
btn_state – The button state (0 = button released, 1 = button pressed)
-
TouchpadData touchpad_convert(const TouchpadData &data) const
Convert touchpad data from raw reading to display coordinates
Note
Uses the touch_invert_x and touch_invert_y settings to determine if the x and y coordinates should be inverted
- Parameters
data – The touchpad data to convert
- Returns
The converted touchpad data
-
bool initialize_lcd()
Initialize the LCD (low level display driver)
- Returns
true if the LCD was successfully initialized, false otherwise
-
bool initialize_display(size_t pixel_buffer_size)
Initialize the display (lvgl display driver)
Note
This will also allocate two full frame buffers in the SPIRAM
- Parameters
pixel_buffer_size – The size of the pixel buffer
- Returns
true if the display was successfully initialized, false otherwise
-
inline std::shared_ptr<Display<Pixel>> display() const
Get a shared pointer to the display
- Returns
A shared pointer to the display
-
void brightness(float brightness)
Set the brightness of the backlight
- Parameters
brightness – The brightness of the backlight as a percentage (0 - 100)
-
float brightness() const
Get the brightness of the backlight
- Returns
The brightness of the backlight as a percentage (0 - 100)
-
Pixel *vram0() const
Get the VRAM 0 pointer (DMA memory used by LVGL)
Note
This is the memory used by LVGL for rendering
Note
This is null unless initialize_display() has been called
- Returns
The VRAM 0 pointer
-
Pixel *vram1() const
Get the VRAM 1 pointer (DMA memory used by LVGL)
Note
This is the memory used by LVGL for rendering
Note
This is null unless initialize_display() has been called
- Returns
The VRAM 1 pointer
-
uint8_t *frame_buffer0() const
Get the frame buffer 0 pointer
Note
This memory is designed to be used by the application developer and is provided as a convenience. It is not used by the display driver.
Note
This is null unless initialize_display() has been called
- Returns
The frame buffer 0 pointer
-
uint8_t *frame_buffer1() const
Get the frame buffer 1 pointer
Note
This memory is designed to be used by the application developer and is provided as a convenience. It is not used by the display driver.
Note
This is null unless initialize_display() has been called
- Returns
The frame buffer 1 pointer
-
void write_command(uint8_t command, std::span<const uint8_t> parameters, uint32_t user_data)
Write command and optional parameters to the LCD
Note
This method is designed to be used by the display driver
Note
This method queues the data to be written to the LCD, only blocking if there is an ongoing SPI transaction
- Parameters
command – The command to write
parameters – The command parameters to write
user_data – User data to pass to the spi transaction callback
-
void write_lcd_frame(const uint16_t x, const uint16_t y, const uint16_t width, const uint16_t height, uint8_t *data)
Write a frame to the LCD
Note
This method queues the data to be written to the LCD, only blocking if there is an ongoing SPI transaction
- Parameters
x – The x coordinate
y – The y coordinate
width – The width of the frame, in pixels
height – The height of the frame, in pixels
data – The data to write
-
void write_lcd_lines(int xs, int ys, int xe, int ye, const uint8_t *data, uint32_t user_data)
Write lines to the LCD
Note
This method queues the data to be written to the LCD, only blocking if there is an ongoing SPI transaction
- Parameters
xs – The x start coordinate
ys – The y start coordinate
xe – The x end coordinate
ye – The y end coordinate
data – The data to write
user_data – User data to pass to the spi transaction callback
-
bool initialize_boot_button(const button_callback_t &callback = nullptr)
Initialize the boot button (side of the box)
- Parameters
callback – The callback function to call when the button is pressed
- Returns
true if the button was successfully initialized, false otherwise
-
bool boot_button_state() const
Get the boot button state
- Returns
The button state (true = button pressed, false = button released)
-
bool initialize_mute_button(const button_callback_t &callback = nullptr)
Initialize the mute button (top of the box)
- Parameters
callback – The callback function to call when the button is pressed
- Returns
true if the button was successfully initialized, false otherwise
-
bool mute_button_state() const
Get the mute button state
- Returns
The button state (true = button pressed, false = button released)
- bool initialize_sound (uint32_t default_audio_rate=48000, const espp::Task::BaseConfig &task_config={ .name="audio",.stack_size_bytes=4096,.priority=19,.core_id=1, })
Initialize the sound subsystem
- Parameters
default_audio_rate – The default audio rate
task_config – The task configuration for the audio task
- Returns
true if the sound subsystem was successfully initialized, false otherwise
-
void enable_sound(bool enable)
Enable or disable sound
Note
This method sets the power pin to the appropriate value
-
uint32_t audio_sample_rate() const
Get the audio sample rate
- Returns
The audio sample rate, in Hz
-
void audio_sample_rate(uint32_t sample_rate)
Set the audio sample rate
- Parameters
sample_rate – The audio sample rate, in Hz
-
size_t audio_buffer_size() const
Get the audio buffer size
- Returns
The audio buffer size, in bytes
-
void mute(bool mute)
Mute or unmute the audio
- Parameters
mute – true to mute the audio, false to unmute the audio
-
bool is_muted() const
Check if the audio is muted
- Returns
true if the audio is muted, false otherwise
-
void volume(float volume)
Set the volume
- Parameters
volume – The volume in percent (0 - 100)
-
float volume() const
Get the volume
- Returns
The volume in percent (0 - 100)
-
void play_audio(const std::vector<uint8_t> &data)
Play audio
- Parameters
data – The audio data to play
-
void play_audio(const uint8_t *data, uint32_t num_bytes)
Play audio
- Parameters
data – The audio data to play
num_bytes – The number of bytes to play
- bool initialize_imu (const Imu::filter_fn &orientation_filter=nullptr, const Imu::ImuConfig &imu_config={ .accelerometer_range=Imu::AccelerometerRange::RANGE_2G,.accelerometer_odr=Imu::AccelerometerODR::ODR_400_HZ,.gyroscope_range=Imu::GyroscopeRange::RANGE_2000DPS,.gyroscope_odr=Imu::GyroscopeODR::ODR_400_HZ, })
Initialize the IMU
- Parameters
orientation_filter – The orientation filter, if provided
imu_config – The IMU configuration
- Returns
true if the IMU was successfully initialized, false otherwise
-
inline const std::string &get_name() const
Get the name of the component
Note
This is the tag of the logger
- Returns
A const reference to the name of the component
-
inline void set_log_tag(const std::string_view &tag)
Set the tag for the logger
- Parameters
tag – The tag to use for the logger
-
inline espp::Logger::Verbosity get_log_level() const
Get the log level for the logger
See also
See also
- Returns
The verbosity level of the logger
-
inline void set_log_level(espp::Logger::Verbosity level)
Set the log level for the logger
See also
See also
- Parameters
level – The verbosity level to use for the logger
-
inline void set_log_verbosity(espp::Logger::Verbosity level)
Set the log verbosity for the logger
See also
See also
See also
Note
This is a convenience method that calls set_log_level
- Parameters
level – The verbosity level to use for the logger
-
inline espp::Logger::Verbosity get_log_verbosity() const
Get the log verbosity for the logger
See also
See also
See also
Note
This is a convenience method that calls get_log_level
- Returns
The verbosity level of the logger
-
inline void set_log_rate_limit(std::chrono::duration<float> rate_limit)
Set the rate limit for the logger
See also
Note
Only calls to the logger that have _rate_limit suffix will be rate limited
- Parameters
rate_limit – The rate limit to use for the logger
Public Static Functions
-
static inline EspBox &get()
Access the singleton instance of the EspBox class.
- Returns
Reference to the singleton instance of the EspBox class
-
static inline constexpr size_t lcd_width()
Get the width of the LCD in pixels
- Returns
The width of the LCD in pixels
-
static inline constexpr size_t lcd_height()
Get the height of the LCD in pixels
- Returns
The height of the LCD in pixels
-
static inline constexpr auto get_lcd_dc_gpio()
Get the GPIO pin for the LCD data/command signal
- Returns
The GPIO pin for the LCD data/command signal
-
static inline constexpr auto get_mute_pin()
Get the GPIO pin for the mute button (top of the box)
- Returns
The GPIO pin for the mute button