-
Notifications
You must be signed in to change notification settings - Fork 24
feat(expressive_eyes): Add new ExpressiveEyes component
#587
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
…k with other hardware
|
⚡ Static analysis result ⚡ 🔴 cppcheck found 4 issues! Click here to see details.espp/components/expressive_eyes/example/main/full_featured_drawer.hpp Lines 62 to 67 in 8e3a541
!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 functionespp/components/expressive_eyes/example/main/full_featured_drawer.hpp Lines 43 to 48 in 8e3a541
!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
!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
!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 |
There was a problem hiding this 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.
| 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; |
Copilot
AI
Jan 24, 2026
There was a problem hiding this comment.
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.
| 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 |
| 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_); | ||
| } |
Copilot
AI
Jan 24, 2026
There was a problem hiding this comment.
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_).
| #include <chrono> | ||
| #include <cmath> | ||
| #include <functional> | ||
| #include <memory> |
Copilot
AI
Jan 24, 2026
There was a problem hiding this comment.
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.
| #include <memory> | |
| #include <memory> | |
| #include <cstdint> | |
| #include <algorithm> |
| 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) |
Copilot
AI
Jan 24, 2026
There was a problem hiding this comment.
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).
| 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 |
Copilot
AI
Jan 24, 2026
There was a problem hiding this comment.
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).
| // 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) { |
Copilot
AI
Jan 24, 2026
There was a problem hiding this comment.
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.
| 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, |
Copilot
AI
Jan 24, 2026
There was a problem hiding this comment.
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.
| 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, |
There was a problem hiding this 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.
Description
expressive_eyescomponent for configurable expressions with support for custom drawing styles.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/exampleonESP32-S3-BOXandwaveshare touch LCDScreenshots (if appropriate, e.g. schematic, board, console logs, lab pictures):
Videos:
Expressive.Eyes.2.compressed.mp4
Console output:
Types of changes
Checklist:
Software
.github/workflows/build.ymlfile to add my new test to the automated cloud build github action.