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>
403 lines
9.1 KiB
C
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;
|
|
}
|