saltylab-firmware/src/face_uart.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

176 lines
5.2 KiB
C

/*
* face_uart.c — UART Command Interface for Face Animations
*
* Receives emotion commands from Jetson Orin and triggers face animations.
* Text-based protocol over USART3 @ 115200 baud.
*/
#include "face_uart.h"
#include "face_animation.h"
#include <string.h>
#include <ctype.h>
#include <stdio.h>
/* === Ring Buffer State === */
static struct {
uint8_t buf[FACE_UART_RX_BUF_SZ];
uint16_t head; /* Write index (ISR) */
uint16_t tail; /* Read index (process) */
uint16_t count; /* Bytes in buffer */
} rx_buf = {0};
/* === Forward Declarations === */
static void uart_send_response(const char *cmd, const char *status);
/* === Private Functions === */
/**
* Extract a line from RX buffer (newline-terminated).
* Returns length if found, 0 otherwise.
*/
static uint16_t extract_line(char *line, uint16_t max_len) {
uint16_t len = 0;
uint16_t idx = rx_buf.tail;
/* Scan for newline */
while (idx != rx_buf.head && len < max_len - 1) {
uint8_t byte = rx_buf.buf[idx];
if (byte == '\n') {
/* Found end of line */
for (uint16_t i = 0; i < len; i++) {
line[i] = rx_buf.buf[(rx_buf.tail + i) % FACE_UART_RX_BUF_SZ];
}
line[len] = '\0';
/* Trim trailing whitespace */
while (len > 0 && (line[len - 1] == '\r' || isspace(line[len - 1]))) {
line[--len] = '\0';
}
/* Update tail and count */
rx_buf.tail = (idx + 1) % FACE_UART_RX_BUF_SZ;
rx_buf.count -= (len + 1 + 1); /* +1 for newline, +1 for any preceding data */
return len;
}
len++;
idx = (idx + 1) % FACE_UART_RX_BUF_SZ;
}
return 0; /* No complete line */
}
/**
* Convert string to uppercase for case-insensitive command matching.
*/
static void str_toupper(char *str) {
for (int i = 0; str[i]; i++)
str[i] = toupper((unsigned char)str[i]);
}
/**
* Parse and execute a command.
*/
static void parse_command(const char *cmd) {
if (!cmd || !cmd[0])
return;
char cmd_upper[32];
strncpy(cmd_upper, cmd, sizeof(cmd_upper) - 1);
cmd_upper[sizeof(cmd_upper) - 1] = '\0';
str_toupper(cmd_upper);
/* Command dispatch */
if (strcmp(cmd_upper, "HAPPY") == 0) {
face_animation_set_emotion(FACE_HAPPY);
uart_send_response(cmd_upper, "OK");
} else if (strcmp(cmd_upper, "SAD") == 0) {
face_animation_set_emotion(FACE_SAD);
uart_send_response(cmd_upper, "OK");
} else if (strcmp(cmd_upper, "CURIOUS") == 0) {
face_animation_set_emotion(FACE_CURIOUS);
uart_send_response(cmd_upper, "OK");
} else if (strcmp(cmd_upper, "ANGRY") == 0) {
face_animation_set_emotion(FACE_ANGRY);
uart_send_response(cmd_upper, "OK");
} else if (strcmp(cmd_upper, "SLEEP") == 0 ||
strcmp(cmd_upper, "SLEEPING") == 0) {
face_animation_set_emotion(FACE_SLEEPING);
uart_send_response(cmd_upper, "OK");
} else if (strcmp(cmd_upper, "NEUTRAL") == 0) {
face_animation_set_emotion(FACE_NEUTRAL);
uart_send_response(cmd_upper, "OK");
} else if (strcmp(cmd_upper, "BLINK") == 0) {
face_animation_blink_now();
uart_send_response(cmd_upper, "OK");
} else if (strcmp(cmd_upper, "STATUS") == 0) {
const char *emotion_names[] = {"HAPPY", "SAD", "CURIOUS", "ANGRY",
"SLEEPING", "NEUTRAL"};
face_emotion_t current = face_animation_get_emotion();
char status[64];
snprintf(status, sizeof(status), "EMOTION=%s, IDLE=%s",
emotion_names[current],
face_animation_is_idle() ? "true" : "false");
uart_send_response(cmd_upper, status);
} else {
uart_send_response(cmd_upper, "ERR: unknown command");
}
}
/**
* Send a response string to UART TX.
* Format: "CMD: status\n"
*/
static void uart_send_response(const char *cmd, const char *status) {
/* TODO: Implement UART TX
* Use HAL_UART_Transmit_IT or similar to send:
* "CMD: status\n"
*/
(void)cmd; /* Suppress unused warnings */
(void)status;
}
/* === Public API Implementation === */
void face_uart_init(void) {
/* TODO: Configure USART3 @ 115200 baud
* - Enable USART3 clock (RCC_APB1ENR)
* - Configure pins (PB10=TX, PB11=RX)
* - Set baud rate to 115200
* - Enable RX interrupt (NVIC + USART3_IRQn)
* - Enable USART
*/
rx_buf.head = 0;
rx_buf.tail = 0;
rx_buf.count = 0;
}
void face_uart_process(void) {
char line[128];
uint16_t len;
/* Extract and process complete commands */
while ((len = extract_line(line, sizeof(line))) > 0) {
parse_command(line);
}
}
void face_uart_rx_isr(uint8_t byte) {
/* Push byte into ring buffer */
if (rx_buf.count < FACE_UART_RX_BUF_SZ) {
rx_buf.buf[rx_buf.head] = byte;
rx_buf.head = (rx_buf.head + 1) % FACE_UART_RX_BUF_SZ;
rx_buf.count++;
}
/* Buffer overflow: silently discard oldest byte */
}
void face_uart_send(const char *str) {
/* TODO: Implement non-blocking UART TX
* Use HAL_UART_Transmit_IT() or DMA-based TX queue.
*/
(void)str; /* Suppress unused warnings */
}