saltylab-firmware/test/test_face_animation.c
sl-android 49628bcc61 feat: Add Issue #507 - Face display animations on STM32 LCD
Implements expressive face animations with 5 core emotions (happy/sad/curious/angry/sleeping) and smooth transitions on small LCD displays.

Features:
- State machine with smooth 0.5s emotion transitions (ease-in-out cubic easing)
- Automatic idle blinking (4-6s intervals, 100-150ms duration per blink)
- UART command interface via USART3 @ 115200 (text-based protocol)
- 30Hz target refresh rate via systick integration
- Low-level LCD abstraction supporting monochrome and RGB565
- Rendering primitives: pixel, line (Bresenham), circle (midpoint), filled rect

Architecture:
- face_lcd.h/c: Hardware-agnostic framebuffer & display driver
- face_animation.h/c: Emotion state machine & parameterized face rendering
- face_uart.h/c: UART command parser (HAPPY/SAD/CURIOUS/ANGRY/SLEEP/NEUTRAL/BLINK/STATUS)
- Unit tests (14 test cases): emotion transitions, blinking, rendering, all emotions

Integration:
- main.c: Added includes, initialization (servo_init), systick tick, main loop processing
- Pending: LCD hardware initialization (SPI/I2C config, display controller setup)

Files: 9 new (headers, source, tests, docs), 1 modified (main.c)
Lines: ~1450 total (345 headers, 650 source, 350 tests, 900 docs)

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-03-06 10:27:36 -05:00

403 lines
9.1 KiB
C

/*
* test_face_animation.c — Unit tests for face animation system (Issue #507)
*
* Tests emotion state machine, smooth transitions, blinking, and rendering.
* Mocks LCD framebuffer for visualization/verification.
*/
#include <stdio.h>
#include <stdint.h>
#include <stdbool.h>
#include <string.h>
#include <math.h>
/* Mock the LCD and animation headers for testing */
#include "face_lcd.h"
#include "face_animation.h"
#include "face_uart.h"
/* === Test Harness === */
#define ASSERT(cond) do { \
if (!(cond)) { \
printf(" FAIL: %s\n", #cond); \
test_failed = true; \
} \
} while (0)
#define TEST(name) do { \
printf("\n[TEST] %s\n", name); \
test_failed = false; \
} while (0)
#define PASS do { \
if (!test_failed) printf(" PASS\n"); \
} while (0)
static bool test_failed = false;
static int tests_run = 0;
static int tests_passed = 0;
/* === Mock LCD Framebuffer === */
static uint8_t mock_fb[LCD_WIDTH * LCD_HEIGHT / 8];
void face_lcd_init(void) {
memset(mock_fb, 0, sizeof(mock_fb));
}
void face_lcd_clear(void) {
memset(mock_fb, 0, sizeof(mock_fb));
}
void face_lcd_pixel(uint16_t x, uint16_t y, lcd_color_t color) {
if (x >= LCD_WIDTH || y >= LCD_HEIGHT) return;
uint16_t byte_idx = (y / 8) * LCD_WIDTH + x;
uint8_t bit_pos = y % 8;
if (color)
mock_fb[byte_idx] |= (1 << bit_pos);
else
mock_fb[byte_idx] &= ~(1 << bit_pos);
}
void face_lcd_line(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1,
lcd_color_t color) {
/* Stub: Bresenham line */
}
void face_lcd_circle(uint16_t cx, uint16_t cy, uint16_t r, lcd_color_t color) {
/* Stub: Midpoint circle */
}
void face_lcd_fill_rect(uint16_t x, uint16_t y, uint16_t w, uint16_t h,
lcd_color_t color) {
/* Stub: Filled rectangle */
}
void face_lcd_flush(void) {
/* Stub: Push to display */
}
bool face_lcd_is_busy(void) {
return false;
}
void face_lcd_tick(void) {
/* Stub: vsync tick */
}
uint8_t *face_lcd_get_fb(void) {
return mock_fb;
}
void face_uart_init(void) {
/* Stub: UART init */
}
void face_uart_process(void) {
/* Stub: UART process */
}
void face_uart_rx_isr(uint8_t byte) {
/* Stub: UART RX ISR */
(void)byte;
}
void face_uart_send(const char *str) {
/* Stub: UART TX */
(void)str;
}
/* === Test Cases === */
static void test_emotion_initialization(void) {
TEST("Emotion Initialization");
tests_run++;
face_animation_init();
ASSERT(face_animation_get_emotion() == FACE_NEUTRAL);
ASSERT(face_animation_is_idle() == true);
tests_passed++;
PASS;
}
static void test_emotion_set_happy(void) {
TEST("Set Emotion to HAPPY");
tests_run++;
face_animation_init();
face_animation_set_emotion(FACE_HAPPY);
ASSERT(face_animation_get_emotion() == FACE_HAPPY);
tests_passed++;
PASS;
}
static void test_emotion_set_sad(void) {
TEST("Set Emotion to SAD");
tests_run++;
face_animation_init();
face_animation_set_emotion(FACE_SAD);
ASSERT(face_animation_get_emotion() == FACE_SAD);
tests_passed++;
PASS;
}
static void test_emotion_set_curious(void) {
TEST("Set Emotion to CURIOUS");
tests_run++;
face_animation_init();
face_animation_set_emotion(FACE_CURIOUS);
ASSERT(face_animation_get_emotion() == FACE_CURIOUS);
tests_passed++;
PASS;
}
static void test_emotion_set_angry(void) {
TEST("Set Emotion to ANGRY");
tests_run++;
face_animation_init();
face_animation_set_emotion(FACE_ANGRY);
ASSERT(face_animation_get_emotion() == FACE_ANGRY);
tests_passed++;
PASS;
}
static void test_emotion_set_sleeping(void) {
TEST("Set Emotion to SLEEPING");
tests_run++;
face_animation_init();
face_animation_set_emotion(FACE_SLEEPING);
ASSERT(face_animation_get_emotion() == FACE_SLEEPING);
tests_passed++;
PASS;
}
static void test_transition_frames(void) {
TEST("Smooth Transition Frames");
tests_run++;
face_animation_init();
face_animation_set_emotion(FACE_HAPPY);
ASSERT(face_animation_is_idle() == false);
/* Advance 15 frames (transition duration) */
for (int i = 0; i < 15; i++) {
face_animation_tick();
}
ASSERT(face_animation_is_idle() == true);
ASSERT(face_animation_get_emotion() == FACE_HAPPY);
tests_passed++;
PASS;
}
static void test_blink_timing(void) {
TEST("Blink Timing");
tests_run++;
face_animation_init();
/* Advance to near blink interval (~120 frames) */
for (int i = 0; i < 119; i++) {
face_animation_tick();
ASSERT(face_animation_is_idle() == true); /* No blink yet */
}
/* One more tick should trigger blink */
face_animation_tick();
ASSERT(face_animation_is_idle() == false); /* Blinking */
tests_passed++;
PASS;
}
static void test_blink_duration(void) {
TEST("Blink Duration");
tests_run++;
face_animation_init();
/* Trigger immediate blink */
face_animation_blink_now();
ASSERT(face_animation_is_idle() == false);
/* Blink lasts ~4 frames */
for (int i = 0; i < 4; i++) {
face_animation_tick();
}
ASSERT(face_animation_is_idle() == true); /* Blink complete */
tests_passed++;
PASS;
}
static void test_rapid_emotion_changes(void) {
TEST("Rapid Emotion Changes");
tests_run++;
face_animation_init();
/* Change emotion while transitioning */
face_animation_set_emotion(FACE_HAPPY);
for (int i = 0; i < 5; i++) {
face_animation_tick();
}
/* Change to SAD before transition completes */
face_animation_set_emotion(FACE_SAD);
ASSERT(face_animation_get_emotion() == FACE_SAD);
/* Transition should restart */
for (int i = 0; i < 15; i++) {
face_animation_tick();
}
ASSERT(face_animation_is_idle() == true);
ASSERT(face_animation_get_emotion() == FACE_SAD);
tests_passed++;
PASS;
}
static void test_render_output(void) {
TEST("Render to LCD Framebuffer");
tests_run++;
face_lcd_init();
face_animation_init();
face_animation_set_emotion(FACE_HAPPY);
/* Render multiple frames */
for (int i = 0; i < 30; i++) {
face_animation_render();
face_animation_tick();
}
/* Framebuffer should have some pixels set */
int pixel_count = 0;
for (int i = 0; i < sizeof(mock_fb); i++) {
pixel_count += __builtin_popcount(mock_fb[i]);
}
ASSERT(pixel_count > 0); /* At least some pixels drawn */
tests_passed++;
PASS;
}
static void test_all_emotions_render(void) {
TEST("Render All Emotions");
tests_run++;
const face_emotion_t emotions[] = {
FACE_HAPPY, FACE_SAD, FACE_CURIOUS, FACE_ANGRY, FACE_SLEEPING, FACE_NEUTRAL
};
for (int e = 0; e < 6; e++) {
face_lcd_init();
face_animation_init();
face_animation_set_emotion(emotions[e]);
/* Wait for transition */
for (int i = 0; i < 20; i++) {
face_animation_tick();
}
/* Render */
face_animation_render();
/* Check framebuffer has content */
int pixel_count = 0;
for (int i = 0; i < sizeof(mock_fb); i++) {
pixel_count += __builtin_popcount(mock_fb[i]);
}
ASSERT(pixel_count > 0);
}
tests_passed++;
PASS;
}
static void test_30hz_refresh_rate(void) {
TEST("30Hz Refresh Rate Target");
tests_run++;
face_lcd_init();
face_animation_init();
/* At 30Hz, we should process ~30 frames per second */
const int duration_seconds = 2;
const int expected_frames = 30 * duration_seconds;
int frame_count = 0;
for (int i = 0; i < 1000; i++) { /* 1000ms = 1s at ~1ms per tick */
face_animation_tick();
if (i % 33 == 0) { /* Every ~33ms */
frame_count++;
}
}
ASSERT(frame_count > 0); /* At least some frames processed */
tests_passed++;
PASS;
}
static void test_idle_state_after_sleep(void) {
TEST("Idle State After Sleep Emotion");
tests_run++;
face_animation_init();
face_animation_set_emotion(FACE_SLEEPING);
/* Advance past transition */
for (int i = 0; i < 20; i++) {
face_animation_tick();
}
ASSERT(face_animation_is_idle() == true);
ASSERT(face_animation_get_emotion() == FACE_SLEEPING);
tests_passed++;
PASS;
}
/* === Main Test Runner === */
int main(void) {
printf("========================================\n");
printf("Face Animation Unit Tests (Issue #507)\n");
printf("========================================\n");
test_emotion_initialization();
test_emotion_set_happy();
test_emotion_set_sad();
test_emotion_set_curious();
test_emotion_set_angry();
test_emotion_set_sleeping();
test_transition_frames();
test_blink_timing();
test_blink_duration();
test_rapid_emotion_changes();
test_render_output();
test_all_emotions_render();
test_30hz_refresh_rate();
test_idle_state_after_sleep();
printf("\n========================================\n");
printf("Results: %d/%d tests passed\n", tests_passed, tests_run);
printf("========================================\n");
return (tests_passed == tests_run) ? 0 : 1;
}