Files
StackChan/remote/code/main/ui/ui_imu_screen.c
T
2026-03-25 11:53:49 +08:00

280 lines
9.6 KiB
C

#include "ui_imu_screen.h"
#include <math.h>
#include <stdio.h>
#include "../lvgl_port.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
static lv_obj_t *edge_lines[12];
static lv_obj_t *cross_lines[2]; // 标识用的对角线
static lv_point_t edge_points[12][2]; // 12条边,每条2个点
static lv_point_t cross_points[2][2]; // 2条对角线,每条2个点
lv_obj_t *imu_screen;
lv_obj_t *cube_container = NULL;
lv_obj_t *imu_battery_label;
lv_obj_t *imu_channel_info_label;
lv_obj_t *imu_id_info_label;
lv_obj_t *imu_data_label;
// 3D cube parameters
typedef struct {
float x, y, z;
} Point3D;
typedef struct {
float x, y;
} Point2D;
// Define the 8 vertices of a cube
static Point3D vertices[8] = {{-1, -1, -1}, {1, -1, -1}, {1, 1, -1}, {-1, 1, -1},
{-1, -1, 1}, {1, -1, 1}, {1, 1, 1}, {-1, 1, 1}};
// Define the 12 edges of a cube (indices of vertices connected)
static int edges[12][2] = {
{0, 1}, {1, 2}, {2, 3}, {3, 0}, // fornt
{4, 5}, {5, 6}, {6, 7}, {7, 4}, // behind
{0, 4}, {1, 5}, {2, 6}, {3, 7} // middle connecting line
};
IMU_Angle_t g_imu_angle = {0.0f, 0.0f};
/**
* @brief Creates the IMU screen with all UI elements including a 3D cube visualization
*
* This function initializes and creates the main IMU screen interface with:
* - Title label showing "StackChan :)"
* - A cube container for 3D visualization
* - 12 cube edge lines forming a 3D cube
* - 2 diagonal cross lines for orientation reference
* - Battery status label
* - Channel information label
* - Receiver ID label
*
* The function handles LVGL locking to ensure thread-safe operations.
*/
void create_imu_screen()
{
while (!lvgl_port_lock()) {
vTaskDelay(pdMS_TO_TICKS(10));
}
if (imu_screen == NULL) {
imu_screen = lv_obj_create(NULL);
}
lv_obj_clear_flag(imu_screen, LV_OBJ_FLAG_SCROLLABLE);
// Create title
lv_obj_t *label = lv_label_create(imu_screen);
lv_label_set_text(label, "StackChan :)");
lv_obj_align(label, LV_ALIGN_TOP_MID, 0, 10);
lv_obj_set_style_text_font(label, &lv_font_montserrat_14, 0);
// create container
cube_container = lv_obj_create(imu_screen);
lv_obj_set_size(cube_container, 115, 115);
lv_obj_align(cube_container, LV_ALIGN_TOP_MID, 0, 35);
lv_obj_set_style_bg_opa(cube_container, LV_OPA_TRANSP, 0);
lv_obj_set_style_border_width(cube_container, 0, 0);
lv_obj_set_style_pad_all(cube_container, 0, 0);
lv_obj_clear_flag(cube_container, LV_OBJ_FLAG_SCROLLABLE);
// Create 12 cube edges
for (int i = 0; i < 12; i++) {
edge_lines[i] = lv_line_create(cube_container);
lv_obj_set_style_line_width(edge_lines[i], 2, 0);
lv_obj_set_style_line_color(edge_lines[i], lv_color_black(), 0);
lv_obj_add_flag(edge_lines[i], LV_OBJ_FLAG_FLOATING);
// Add default coordinate points to avoid empty line segments
lv_point_t default_points[2] = {{0, 0}, {0, 0}};
lv_line_set_points(edge_lines[i], default_points, 2);
}
// Create 2 lines identifying the diagonals
for (int i = 0; i < 2; i++) {
cross_lines[i] = lv_line_create(cube_container);
lv_obj_set_style_line_width(cross_lines[i], 2, 0);
lv_obj_set_style_line_color(cross_lines[i], lv_color_make(0, 255, 255), 0);
lv_obj_add_flag(cross_lines[i], LV_OBJ_FLAG_FLOATING);
// Add default coordinate
lv_point_t default_cross_points[2] = {{0, 0}, {0, 0}};
lv_line_set_points(cross_lines[i], default_cross_points, 2);
}
// Create other UI elements
imu_battery_label = lv_label_create(imu_screen);
lv_label_set_text(imu_battery_label, "Bat: 100%");
lv_obj_align(imu_battery_label, LV_ALIGN_TOP_LEFT, 10, 160);
lv_obj_set_style_text_font(imu_battery_label, &lv_font_montserrat_14, 0);
imu_channel_info_label = lv_label_create(imu_screen);
lv_label_set_text(imu_channel_info_label, "Channel: 1");
lv_obj_align(imu_channel_info_label, LV_ALIGN_TOP_LEFT, 10, 180);
lv_obj_set_style_text_font(imu_channel_info_label, &lv_font_montserrat_14, 0);
imu_id_info_label = lv_label_create(imu_screen);
lv_label_set_text(imu_id_info_label, "Receiver ID:\n0(broadcast)");
lv_obj_align(imu_id_info_label, LV_ALIGN_TOP_LEFT, 10, 200);
lv_obj_set_style_text_font(imu_id_info_label, &lv_font_montserrat_14, 0);
lvgl_port_unlock();
}
/**
* @brief Updates the 3D cube visualization based on IMU accelerometer data
*
* This function takes accelerometer readings (ax, ay, az) and calculates
* the pitch and roll angles to rotate a 3D cube representation. It performs:
* - Input validation to check for NaN or infinite values
* - Calculation of pitch and roll angles using trigonometric functions
* - 3D to 2D projection of cube vertices with rotation transformations
* - Updates all 12 cube edge lines and 2 diagonal marker lines
*
* @param ax Accelerometer X-axis reading
* @param ay Accelerometer Y-axis reading
* @param az Accelerometer Z-axis reading
*/
void update_imu_cube(float ax, float ay, float az)
{
// Check if the input value is valid
if (isnan(ax) || isnan(ay) || isnan(az) || isinf(ax) || isinf(ay) || isinf(az)) {
printf("Invalid IMU data received!\n");
return;
}
// Calculate tilt angle (based on gravitational acceleration)
float pitch = atan2(ay, sqrt(ax * ax + az * az));
float roll = atan2(ax, sqrt(ay * ay + az * az));
if (az < 0) {
pitch = M_PI - pitch;
}
g_imu_angle.pitch = pitch;
g_imu_angle.roll = roll;
// 3D projection calculation
Point2D projected[8]; // Store the 2D points after projection
int centerX = lv_obj_get_width(cube_container) / 2;
int centerY = lv_obj_get_height(cube_container) / 2;
float scale = 30.0f;
for (int i = 0; i < 8; i++) {
Point3D p = vertices[i];
// Pitch
float y1 = p.y * cos(pitch) - p.z * sin(pitch);
float z1 = p.y * sin(pitch) + p.z * cos(pitch);
float x1 = p.x;
// Roll
float x2 = x1 * cos(roll) + z1 * sin(roll);
float z2 = -x1 * sin(roll) + z1 * cos(roll);
float y2 = y1;
// Orthographic projection
projected[i].x = centerX + x2 * scale;
projected[i].y = centerY + y2 * scale;
}
// Update 12 cube edges
for (int i = 0; i < 12; i++) {
edge_points[i][0].x = (int16_t)projected[edges[i][0]].x;
edge_points[i][0].y = (int16_t)projected[edges[i][0]].y;
edge_points[i][1].x = (int16_t)projected[edges[i][1]].x;
edge_points[i][1].y = (int16_t)projected[edges[i][1]].y;
lv_line_set_points(edge_lines[i], edge_points[i], 2);
}
// Update 2 diagonal markers
cross_points[0][0] = (lv_point_t){(int16_t)projected[0].x, (int16_t)projected[0].y};
cross_points[0][1] = (lv_point_t){(int16_t)projected[2].x, (int16_t)projected[2].y};
lv_line_set_points(cross_lines[0], cross_points[0], 2);
cross_points[1][0] = (lv_point_t){(int16_t)projected[1].x, (int16_t)projected[1].y};
cross_points[1][1] = (lv_point_t){(int16_t)projected[3].x, (int16_t)projected[3].y};
lv_line_set_points(cross_lines[1], cross_points[1], 2);
}
/**
* @brief Updates the complete IMU screen with sensor data and system information
*
* This function updates the entire IMU screen with real-time data including:
* - Updates the 3D cube visualization via update_imu_cube()
* - Battery percentage display
* - Communication channel information
* - Receiver ID display (handles broadcast case)
*
* Optimized to only update labels when values have changed using static tracking variables.
*
* @param ax Accelerometer X-axis reading
* @param ay Accelerometer Y-axis reading
* @param az Accelerometer Z-axis reading
* @param bat Battery level percentage (0-100)
* @param id Receiver ID (0 for broadcast)
* @param channel ESP-NOW communication channel
* @return IMU_Angle_t Current pitch and roll angles calculated from IMU data
*/
IMU_Angle_t update_imu_screen(float ax, float ay, float az, uint8_t bat, uint8_t id, uint8_t channel)
{
static uint8_t last_bat = 0xFF;
static uint8_t last_id = 0xFF;
static uint8_t last_channel = 0xFF;
while (!lvgl_port_lock()) {
vTaskDelay(pdMS_TO_TICKS(10));
}
update_imu_cube(ax, ay, az);
if (imu_battery_label && bat != last_bat) {
lv_label_set_text_fmt(imu_battery_label, "Bat: %d%%", bat);
last_bat = bat;
}
if (imu_channel_info_label && channel != last_channel) {
lv_label_set_text_fmt(imu_channel_info_label, "Channel: %u", channel);
last_channel = channel;
}
if (imu_id_info_label && id != last_id) {
if (id == 0) {
lv_label_set_text(imu_id_info_label, "Receiver ID:\n0(broadcast)");
} else {
lv_label_set_text_fmt(imu_id_info_label, "Receiver ID: %u", id);
}
last_id = id;
}
lvgl_port_unlock();
return g_imu_angle;
}
/**
* @brief Destroys and cleans up the IMU screen resources
*
* This function safely removes the IMU screen from memory by:
* - Acquiring LVGL lock for thread safety
* - Deleting the main screen object if it exists
* - Setting all UI element pointers to NULL to prevent dangling references
*
* After execution, the screen will need to be recreated before use again.
*/
void ui_imu_screen_destory()
{
while (!lvgl_port_lock()) {
vTaskDelay(pdMS_TO_TICKS(10));
}
if (imu_screen != NULL) {
lv_obj_del(imu_screen);
imu_screen = NULL;
}
lvgl_port_unlock();
imu_battery_label = NULL;
imu_channel_info_label = NULL;
imu_id_info_label = NULL;
imu_data_label = NULL;
}