Skip to content

Conversation

@finger563
Copy link
Contributor

@finger563 finger563 commented Jan 24, 2026

Description

  • Add expressive_eyes component for configurable expressions with support for custom drawing styles.
  • Update CI
  • Update Docs

Motivation and Context

They look nice, and can help to create a nice little desk buddy or robot face

How has this been tested?

Build and run expressive_eyes/example on ESP32-S3-BOX and waveshare touch LCD

Screenshots (if appropriate, e.g. schematic, board, console logs, lab pictures):

happy image sad image
angry image surprised image
looking image looking image

Videos:

Expressive.Eyes.2.compressed.mp4

Console output:

image
I (886) main_task: Calling app_main()
[Expressive Eyes Example/I][0.889]: Starting Expressive Eyes Example
[WsS3Touch/I][0.899]: Initializing LCD...
[WsS3Touch/I][0.900]: Initializing SPI...
[WsS3Touch/I][0.906]: Adding device to SPI bus...
[WsS3Touch/I][0.911]: Initializing the display driver...
[WsS3Touch/I][1.321]: Display driver initialized successfully!
W (1322) ledc: the binded timer can't keep alive in sleep
[WsS3Touch/I][1.324]: Initializing display with pixel buffer size: 12000
[WsS3Touch/I][1.334]: Display initialized successfully!
[Expressive Eyes Example/I][1.335]: Display size: 240x280
[Expressive Eyes Example/I][1.342]: Using Monochrome Blue drawer
[Expressive Eyes Example/I][1.392]: Expressive eyes initialized
[Expressive Eyes Example/I][1.392]: Testing different expressions...
[Expressive Eyes Example/I][1.395]: Expression: Neutral
[Expressive Eyes Example/I][11.928]: Expression: Happy
[Expressive Eyes Example/I][22.548]: Expression: Sad
[Expressive Eyes Example/I][33.168]: Expression: Angry
[Expressive Eyes Example/I][43.788]: Expression: Surprised
[Expressive Eyes Example/I][54.407]: Testing look_at functionality
[Expressive Eyes Example/I][54.408]: Looking left
[Expressive Eyes Example/I][59.717]: Looking right
[Expressive Eyes Example/I][65.028]: Looking up
[Expressive Eyes Example/I][70.337]: Looking down
[Expressive Eyes Example/I][75.648]: Looking center
[Expressive Eyes Example/I][80.957]: Starting random demo mode - will run continuously

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • Documentation Update
  • Hardware (schematic, board, system design) change
  • Software change

Checklist:

  • My change requires a change to the documentation.
  • I have added / updated the documentation related to this change via either README or WIKI

Software

  • I have added tests to cover my changes.
  • I have updated the .github/workflows/build.yml file to add my new test to the automated cloud build github action.
  • All new and existing tests passed.
  • My code follows the code style of this project.

@github-actions
Copy link

github-actions bot commented Jan 24, 2026

⚡ Static analysis result ⚡

🔴 cppcheck found 4 issues! Click here to see details.

virtual void cleanup() override {
// No dynamic resources to free
}
private:
/**

!Line: 62 - style: Virtual function 'cleanup' is called from destructor '~FullFeaturedDrawer()' at line 43. Dynamic binding is not used. [virtualCallInConstructor]

!Line: 43 - note: Calling cleanup
!Line: 62 - note: cleanup is a virtual function

virtual ~FullFeaturedDrawer() { cleanup(); }
virtual DrawCallback get_draw_callback() override {
return [this](const espp::ExpressiveEyes::EyeState &left,
const espp::ExpressiveEyes::EyeState &right) {
std::lock_guard<std::recursive_mutex> lock(lvgl_mutex_);

!Line: 43 - style: The destructor '~FullFeaturedDrawer' overrides a destructor in a base class but is not marked with a 'override' specifier. [missingOverride]

!Line: 21 - note: Virtual destructor in base class
!Line: 43 - note: Destructor in derived class

virtual void cleanup() override {
// No dynamic resources to free
}
private:
/**

!Line: 60 - style: Virtual function 'cleanup' is called from destructor '~MonochromeBlueDrawer()' at line 44. Dynamic binding is not used. [virtualCallInConstructor]

!Line: 44 - note: Calling cleanup
!Line: 60 - note: cleanup is a virtual function

virtual ~MonochromeBlueDrawer() { cleanup(); }
virtual DrawCallback get_draw_callback() override {
return [this](const espp::ExpressiveEyes::EyeState &left,
const espp::ExpressiveEyes::EyeState &right) {
std::lock_guard<std::recursive_mutex> lock(lvgl_mutex_);

!Line: 44 - style: The destructor '~MonochromeBlueDrawer' overrides a destructor in a base class but is not marked with a 'override' specifier. [missingOverride]

!Line: 21 - note: Virtual destructor in base class
!Line: 44 - note: Destructor in derived class

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new ExpressiveEyes display animation component, along with documentation, an ESP-IDF example app, and CI updates so the new component is built and published.

Changes:

  • Introduces components/expressive_eyes (core class + IDF component metadata).
  • Adds an ESP-IDF example with multiple LVGL-based drawing styles and menuconfig options.
  • Updates Sphinx/Doxygen docs and GitHub Actions workflows to include the new component and example.

Reviewed changes

Copilot reviewed 21 out of 21 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
doc/en/display/index.rst Adds Expressive Eyes docs page into the display documentation toctree.
doc/en/display/expressive_eyes_example.md Includes the example README into the docs.
doc/en/display/expressive_eyes.rst New Expressive Eyes feature overview + API reference include.
doc/Doxyfile Adds the ExpressiveEyes header and example to Doxygen inputs/examples.
components/expressive_eyes/src/expressive_eyes.cpp Implements animation logic (blink, look, expression blending) and draw callback invocation.
components/expressive_eyes/include/expressive_eyes.hpp Public API for ExpressiveEyes (config, states, expressions, callback).
components/expressive_eyes/idf_component.yml Registers the component for ESP-IDF component manager publishing.
components/expressive_eyes/example/sdkconfig.defaults Default config values for the example build.
components/expressive_eyes/example/main/monochrome_blue_drawer.hpp Example LVGL renderer: blue monochrome eyes.
components/expressive_eyes/example/main/full_featured_drawer.hpp Example LVGL renderer: white eyes with pupils and background-cut eyebrows/cheeks.
components/expressive_eyes/example/main/eye_drawer.hpp Drawer interface used by the example to provide draw callbacks.
components/expressive_eyes/example/main/expressive_eyes_example.cpp Example app wiring (board init, LVGL canvas, expression/looking demos).
components/expressive_eyes/example/main/README_DRAWERS.md Documents the drawer architecture and how to add new drawers.
components/expressive_eyes/example/main/Kconfig.projbuild Adds menuconfig options for board + drawing method + eye params.
components/expressive_eyes/example/main/CMakeLists.txt Registers the example’s main component with ESP-IDF.
components/expressive_eyes/example/README.md User-facing instructions and description for the example.
components/expressive_eyes/example/CMakeLists.txt ESP-IDF project config for building the example.
components/expressive_eyes/README.md Component README describing features and linking to the example.
components/expressive_eyes/CMakeLists.txt ESP-IDF component registration for expressive_eyes.
.github/workflows/upload_components.yml Adds components/expressive_eyes to publishing workflow.
.github/workflows/build.yml Adds the expressive_eyes example to CI build matrix.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +318 to +322
state.eye_width_scale = 1.0f;
state.eye_height_scale = 1.0f;
state.eye_rotation = 0.0f;
state.top_curve = 0.5f;
state.bottom_curve = 0.5f;
Copy link

Copilot AI Jan 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WINK_LEFT / WINK_RIGHT are advertised as expressions, but the preset state for these cases is identical to neutral and the draw callback API provides a single shared ExpressionState for both eyes. As a result, the component currently can’t actually wink one eye independently. Consider extending the API to allow per-eye expression overrides (or add a per-eye blink/closed amount in EyeState) and implement the wink presets accordingly.

Suggested change
state.eye_width_scale = 1.0f;
state.eye_height_scale = 1.0f;
state.eye_rotation = 0.0f;
state.top_curve = 0.5f;
state.bottom_curve = 0.5f;
// Represent a wink as a mostly closed eye shape.
state.eye_width_scale = 1.0f;
state.eye_height_scale = 0.25f; // Nearly closed vertically
state.eye_rotation = 0.0f;
state.top_curve = 0.3f;
state.bottom_curve = 0.3f;
state.pupil.enabled = false; // Hide pupil when winking

Copilot uses AI. Check for mistakes.
Comment on lines +112 to +128
void ExpressiveEyes::update_expression(float dt) {
if (current_expression_ != target_expression_) {
// Start blend
current_expression_ = target_expression_;
target_state_ = get_preset_expression(target_expression_);
expression_blend_ = 0.0f;
}

// Blend towards target
if (expression_blend_ < 1.0f) {
expression_blend_ += dt * 3.0f; // Blend over ~0.33 seconds (slower, smoother)
if (expression_blend_ > 1.0f)
expression_blend_ = 1.0f;

ExpressionState from = get_preset_expression(current_expression_);
blend_expression_states(current_state_, from, target_state_, expression_blend_);
}
Copy link

Copilot AI Jan 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

update_expression() overwrites current_expression_ with target_expression_ before computing the from state, so from and target_state_ become the same preset. This effectively disables blending (and makes the blend logic incorrect). Keep a separate “previous/current” expression state to blend from (e.g., store the prior expression or snapshot current_state_ before switching), and only update current_expression_ after the blend completes (or introduce a source_expression_).

Copilot uses AI. Check for mistakes.
#include <chrono>
#include <cmath>
#include <functional>
#include <memory>
Copy link

Copilot AI Jan 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This header isn’t self-contained: it uses uint16_t and std::min/std::max (in clamp) without including the required standard headers. Add the appropriate includes (e.g., <cstdint> and <algorithm>) so consumers can include expressive_eyes.hpp without relying on indirect includes.

Suggested change
#include <memory>
#include <memory>
#include <cstdint>
#include <algorithm>

Copilot uses AI. Check for mistakes.
Comment on lines +44 to +50
struct Eyebrow {
bool enabled{false}; ///< Whether to draw eyebrows
float angle{0.0f}; ///< Eyebrow angle in radians
float height{0.0f}; ///< Vertical offset relative to eye (-1.0 to 1.0)
float thickness{0.1f}; ///< Eyebrow thickness relative to eye
float width{1.0f}; ///< Eyebrow width relative to eye
uint16_t color{0x0000}; ///< Eyebrow color (black)
Copy link

Copilot AI Jan 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Eyebrow::angle is documented as radians, but the preset values are set in degrees (e.g., -20, 25) and the example drawers convert angle * M_PI / 180. This unit mismatch will confuse callback implementers; either change the documentation to degrees or store angles in radians consistently (and update presets/drawers accordingly).

Copilot uses AI. Check for mistakes.
Comment on lines +140 to +152
struct Config {
int screen_width{320}; ///< Screen width in pixels
int screen_height{240}; ///< Screen height in pixels
int eye_spacing{100}; ///< Distance between eye centers
int eye_width{60}; ///< Base eye width
int eye_height{80}; ///< Base eye height
uint16_t eye_color{0xFFFF}; ///< Eye color (white)
float blink_duration{0.12f}; ///< Blink duration in seconds
float blink_interval{4.0f}; ///< Average time between blinks
bool enable_auto_blink{true}; ///< Automatic random blinking
bool enable_pupil_physics{true}; ///< Smooth pupil movement
draw_callback on_draw{nullptr}; ///< Drawing callback
Logger::Verbosity log_level{Logger::Verbosity::WARN}; ///< Log verbosity
Copy link

Copilot AI Jan 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Config::eye_color is exposed as a configuration option, but it’s never used by ExpressiveEyes and isn’t propagated to the draw callback via EyeState/ExpressionState. This makes the API misleading (users can set it but it won’t affect rendering). Either remove it from the config, or include the eye color in the callback data (or otherwise apply it in the component).

Copilot uses AI. Check for mistakes.
Comment on lines +71 to +75
// Allocate buffer for main canvas (RGB565 = 2 bytes per pixel)
static lv_color_t *canvas_buffer = nullptr;
canvas_buffer = (lv_color_t *)heap_caps_malloc(screen_width * screen_height * sizeof(lv_color_t),
MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
if (!canvas_buffer) {
Copy link

Copilot AI Jan 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

heap_caps_malloc is used here, but the file doesn’t include <esp_heap_caps.h>. This will fail to compile unless an unrelated header happens to pull it in. Include the proper header directly in this translation unit.

Copilot uses AI. Check for mistakes.
Comment on lines +110 to +123
int large_eye_width = screen_width * 0.35f; // 35% of screen width each
int large_eye_height = screen_height * 0.55f; // 55% of screen height
int large_spacing = screen_width * 0.55f; // Space between eye centers

espp::ExpressiveEyes::Config config{.screen_width = screen_width,
.screen_height = screen_height,
.eye_spacing = large_spacing,
.eye_width = large_eye_width,
.eye_height = large_eye_height,
.eye_color = 0xFFFF,
.blink_duration = 0.12f,
.blink_interval = 4.0f,
.enable_auto_blink = true,
.enable_pupil_physics = true,
Copy link

Copilot AI Jan 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The example defines many menuconfig options (AUTO_DEMO, interval, eye sizes/colors, blink interval, etc.) in Kconfig.projbuild/sdkconfig.defaults, but this code ignores them and hardcodes values (eye sizing, blink interval, always runs the full scripted demo then infinite random mode). Either wire these CONFIG_EXPRESSIVE_EYES_* settings into the runtime configuration/flow, or remove the unused Kconfig options to avoid drift.

Suggested change
int large_eye_width = screen_width * 0.35f; // 35% of screen width each
int large_eye_height = screen_height * 0.55f; // 55% of screen height
int large_spacing = screen_width * 0.55f; // Space between eye centers
espp::ExpressiveEyes::Config config{.screen_width = screen_width,
.screen_height = screen_height,
.eye_spacing = large_spacing,
.eye_width = large_eye_width,
.eye_height = large_eye_height,
.eye_color = 0xFFFF,
.blink_duration = 0.12f,
.blink_interval = 4.0f,
.enable_auto_blink = true,
.enable_pupil_physics = true,
// Allow menuconfig to override eye geometry if configured, otherwise use defaults.
#ifdef CONFIG_EXPRESSIVE_EYES_EYE_WIDTH
int large_eye_width = CONFIG_EXPRESSIVE_EYES_EYE_WIDTH;
#else
int large_eye_width = screen_width * 0.35f; // 35% of screen width each
#endif
#ifdef CONFIG_EXPRESSIVE_EYES_EYE_HEIGHT
int large_eye_height = CONFIG_EXPRESSIVE_EYES_EYE_HEIGHT;
#else
int large_eye_height = screen_height * 0.55f; // 55% of screen height
#endif
#ifdef CONFIG_EXPRESSIVE_EYES_EYE_SPACING
int large_spacing = CONFIG_EXPRESSIVE_EYES_EYE_SPACING;
#else
int large_spacing = screen_width * 0.55f; // Space between eye centers
#endif
#ifdef CONFIG_EXPRESSIVE_EYES_EYE_COLOR
uint16_t eye_color = CONFIG_EXPRESSIVE_EYES_EYE_COLOR;
#else
uint16_t eye_color = 0xFFFF;
#endif
#ifdef CONFIG_EXPRESSIVE_EYES_BLINK_DURATION
float blink_duration = static_cast<float>(CONFIG_EXPRESSIVE_EYES_BLINK_DURATION);
#else
float blink_duration = 0.12f;
#endif
#ifdef CONFIG_EXPRESSIVE_EYES_BLINK_INTERVAL
float blink_interval = static_cast<float>(CONFIG_EXPRESSIVE_EYES_BLINK_INTERVAL);
#else
float blink_interval = 4.0f;
#endif
#ifdef CONFIG_EXPRESSIVE_EYES_ENABLE_AUTO_BLINK
bool enable_auto_blink = CONFIG_EXPRESSIVE_EYES_ENABLE_AUTO_BLINK;
#else
bool enable_auto_blink = true;
#endif
#ifdef CONFIG_EXPRESSIVE_EYES_ENABLE_PUPIL_PHYSICS
bool enable_pupil_physics = CONFIG_EXPRESSIVE_EYES_ENABLE_PUPIL_PHYSICS;
#else
bool enable_pupil_physics = true;
#endif
espp::ExpressiveEyes::Config config{.screen_width = screen_width,
.screen_height = screen_height,
.eye_spacing = large_spacing,
.eye_width = large_eye_width,
.eye_height = large_eye_height,
.eye_color = eye_color,
.blink_duration = blink_duration,
.blink_interval = blink_interval,
.enable_auto_blink = enable_auto_blink,
.enable_pupil_physics = enable_pupil_physics,

Copilot uses AI. Check for mistakes.
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants