feat: 实现摇一摇导航功能
This commit is contained in:
@@ -3,11 +3,15 @@
|
||||
|
||||
#include "board_config.h"
|
||||
#include "display.h"
|
||||
#include "imu.h"
|
||||
#include "lvgl_port.h"
|
||||
#include "motion_gesture.h"
|
||||
#include "touch.h"
|
||||
#include "ui.h"
|
||||
#include "ui_handlers.h"
|
||||
#include "ui_nav.h"
|
||||
#include "ble_manager.h"
|
||||
#include "settings/settings_store.h"
|
||||
|
||||
#if BOARD_WIFI_ENABLED
|
||||
#include "wifi_manager.h"
|
||||
@@ -38,10 +42,25 @@ void app_main(void)
|
||||
|
||||
lvgl_port_lock();
|
||||
ui_init();
|
||||
ui_nav_init();
|
||||
lvgl_port_unlock();
|
||||
|
||||
settings_store_load();
|
||||
settings_store_apply();
|
||||
|
||||
ui_handlers_init();
|
||||
|
||||
motion_gesture_init(NULL);
|
||||
|
||||
#if BOARD_IMU_ENABLED
|
||||
ret = imu_init();
|
||||
if (ret == ESP_OK) {
|
||||
imu_start_task(motion_gesture_on_sample);
|
||||
} else {
|
||||
ESP_LOGW(TAG, "IMU init failed, shake navigation unavailable");
|
||||
}
|
||||
#endif
|
||||
|
||||
#if BOARD_WIFI_ENABLED
|
||||
ESP_LOGI(TAG, "Starting WiFi");
|
||||
wifi_manager_init();
|
||||
|
||||
@@ -0,0 +1,191 @@
|
||||
#include "motion_gesture.h"
|
||||
|
||||
#include "ui_nav.h"
|
||||
|
||||
#include "esp_log.h"
|
||||
#include "esp_timer.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/queue.h"
|
||||
#include "freertos/task.h"
|
||||
#include "lvgl.h"
|
||||
|
||||
#include <math.h>
|
||||
|
||||
static const char *TAG = "motion";
|
||||
|
||||
#define SAMPLE_RATE_HZ 100
|
||||
#define THRESHOLD_LOW_G 0.6f
|
||||
#define THRESHOLD_MED_G 0.8f
|
||||
#define THRESHOLD_HIGH_G 1.0f
|
||||
#define WINDOW_MS 300
|
||||
#define MIN_SAMPLES 5
|
||||
#define COOLDOWN_MS 800
|
||||
#define CALIBRATION_MS 500
|
||||
|
||||
typedef enum {
|
||||
GESTURE_IDLE = 0,
|
||||
GESTURE_COOLDOWN,
|
||||
} gesture_state_t;
|
||||
|
||||
static QueueHandle_t s_nav_queue;
|
||||
static bool s_enabled = true;
|
||||
static float s_threshold = THRESHOLD_MED_G;
|
||||
static gesture_state_t s_state = GESTURE_IDLE;
|
||||
|
||||
static float s_ref_x;
|
||||
static float s_ref_y;
|
||||
static float s_ref_z;
|
||||
static bool s_calibrated;
|
||||
static int64_t s_cal_start_us;
|
||||
static int s_cal_count;
|
||||
static float s_cal_sum_x;
|
||||
static float s_cal_sum_y;
|
||||
static float s_cal_sum_z;
|
||||
|
||||
static int s_over_count;
|
||||
static int64_t s_window_start_us;
|
||||
static int64_t s_cooldown_until_us;
|
||||
|
||||
static void nav_handler_task(void *arg)
|
||||
{
|
||||
(void)arg;
|
||||
motion_nav_event_t ev;
|
||||
|
||||
while (xQueueReceive(s_nav_queue, &ev, portMAX_DELAY) == pdTRUE) {
|
||||
if (ev == MOTION_NAV_NEXT) {
|
||||
ui_nav_next();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static float magnitude(float ax, float ay, float az)
|
||||
{
|
||||
return sqrtf(ax * ax + ay * ay + az * az);
|
||||
}
|
||||
|
||||
static void post_nav_next(void)
|
||||
{
|
||||
motion_nav_event_t ev = MOTION_NAV_NEXT;
|
||||
if (xQueueSend(s_nav_queue, &ev, 0) != pdTRUE) {
|
||||
ESP_LOGW(TAG, "nav queue full");
|
||||
} else {
|
||||
ESP_LOGI(TAG, "Shake detected -> next screen");
|
||||
}
|
||||
}
|
||||
|
||||
static void run_calibration(float ax, float ay, float az)
|
||||
{
|
||||
s_cal_sum_x += ax;
|
||||
s_cal_sum_y += ay;
|
||||
s_cal_sum_z += az;
|
||||
s_cal_count++;
|
||||
|
||||
int64_t elapsed_us = esp_timer_get_time() - s_cal_start_us;
|
||||
if (elapsed_us >= (int64_t)CALIBRATION_MS * 1000) {
|
||||
if (s_cal_count > 0) {
|
||||
s_ref_x = s_cal_sum_x / (float)s_cal_count;
|
||||
s_ref_y = s_cal_sum_y / (float)s_cal_count;
|
||||
s_ref_z = s_cal_sum_z / (float)s_cal_count;
|
||||
}
|
||||
s_calibrated = true;
|
||||
ESP_LOGI(TAG, "Baseline calibrated (ref=%.3f,%.3f,%.3f g)", s_ref_x, s_ref_y,
|
||||
s_ref_z);
|
||||
}
|
||||
}
|
||||
|
||||
static void reset_window(void)
|
||||
{
|
||||
s_over_count = 0;
|
||||
s_window_start_us = 0;
|
||||
}
|
||||
|
||||
static void process_sample(float ax, float ay, float az)
|
||||
{
|
||||
int64_t now_us = esp_timer_get_time();
|
||||
|
||||
if (!s_calibrated) {
|
||||
run_calibration(ax, ay, az);
|
||||
return;
|
||||
}
|
||||
|
||||
if (s_state == GESTURE_COOLDOWN) {
|
||||
if (now_us >= s_cooldown_until_us) {
|
||||
s_state = GESTURE_IDLE;
|
||||
reset_window();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
float dx = ax - s_ref_x;
|
||||
float dy = ay - s_ref_y;
|
||||
float dz = az - s_ref_z;
|
||||
float mag = magnitude(dx, dy, dz);
|
||||
|
||||
if (mag > s_threshold) {
|
||||
if (s_over_count == 0) {
|
||||
s_window_start_us = now_us;
|
||||
}
|
||||
s_over_count++;
|
||||
|
||||
int64_t window_us = (int64_t)WINDOW_MS * 1000;
|
||||
if ((now_us - s_window_start_us) > window_us) {
|
||||
s_over_count = 1;
|
||||
s_window_start_us = now_us;
|
||||
}
|
||||
|
||||
if (s_over_count >= MIN_SAMPLES) {
|
||||
if (s_enabled) {
|
||||
post_nav_next();
|
||||
}
|
||||
s_state = GESTURE_COOLDOWN;
|
||||
s_cooldown_until_us = now_us + (int64_t)COOLDOWN_MS * 1000;
|
||||
reset_window();
|
||||
}
|
||||
} else if (s_over_count > 0) {
|
||||
int64_t window_us = (int64_t)WINDOW_MS * 1000;
|
||||
if ((now_us - s_window_start_us) > window_us) {
|
||||
reset_window();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void motion_gesture_init(motion_nav_cb_t cb)
|
||||
{
|
||||
(void)cb;
|
||||
|
||||
s_nav_queue = xQueueCreate(4, sizeof(motion_nav_event_t));
|
||||
xTaskCreate(nav_handler_task, "nav_handler", 2048, NULL, 5, NULL);
|
||||
|
||||
s_cal_start_us = esp_timer_get_time();
|
||||
ESP_LOGI(TAG, "Motion gesture ready (calibrating %d ms)", CALIBRATION_MS);
|
||||
}
|
||||
|
||||
void motion_gesture_set_enabled(bool enabled)
|
||||
{
|
||||
s_enabled = enabled;
|
||||
ESP_LOGI(TAG, "Shake navigation %s", enabled ? "enabled" : "disabled");
|
||||
}
|
||||
|
||||
void motion_gesture_set_sensitivity(shake_sensitivity_t sens)
|
||||
{
|
||||
switch (sens) {
|
||||
case SHAKE_SENS_LOW:
|
||||
s_threshold = THRESHOLD_LOW_G;
|
||||
break;
|
||||
case SHAKE_SENS_MED:
|
||||
s_threshold = THRESHOLD_MED_G;
|
||||
break;
|
||||
case SHAKE_SENS_HIGH:
|
||||
s_threshold = THRESHOLD_HIGH_G;
|
||||
break;
|
||||
default:
|
||||
LV_ASSERT_MSG(false, "unexpected shake_sensitivity_t");
|
||||
break;
|
||||
}
|
||||
ESP_LOGI(TAG, "Sensitivity set (threshold=%.2f g)", s_threshold);
|
||||
}
|
||||
|
||||
void motion_gesture_on_sample(float ax_g, float ay_g, float az_g)
|
||||
{
|
||||
process_sample(ax_g, ay_g, az_g);
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdbool.h>
|
||||
|
||||
#include "settings_types.h"
|
||||
|
||||
typedef enum {
|
||||
MOTION_NAV_NEXT = 0,
|
||||
} motion_nav_event_t;
|
||||
|
||||
typedef void (*motion_nav_cb_t)(motion_nav_event_t event);
|
||||
|
||||
void motion_gesture_init(motion_nav_cb_t cb);
|
||||
void motion_gesture_set_enabled(bool enabled);
|
||||
void motion_gesture_set_sensitivity(shake_sensitivity_t sens);
|
||||
void motion_gesture_on_sample(float ax_g, float ay_g, float az_g);
|
||||
@@ -105,6 +105,12 @@ void settings_on_value_changed(lv_event_t *e)
|
||||
case SETTINGS_FIELD_MUTE:
|
||||
buf.mute = lv_obj_has_state(target, LV_STATE_CHECKED);
|
||||
break;
|
||||
case SETTINGS_FIELD_SHAKE_NAV:
|
||||
buf.shake_nav_enabled = lv_obj_has_state(target, LV_STATE_CHECKED);
|
||||
break;
|
||||
case SETTINGS_FIELD_SHAKE_SENS:
|
||||
buf.shake_sensitivity = (shake_sensitivity_t)lv_dropdown_get_selected(target);
|
||||
break;
|
||||
default:
|
||||
LV_ASSERT_MSG(false, "unexpected settings_field_t");
|
||||
break;
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
#include "ui_nav.h"
|
||||
|
||||
#include "lvgl_port.h"
|
||||
#include "screens/ui_Screen1.h"
|
||||
#include "screens/ui_Screen2.h"
|
||||
#include "screens/ui_Screen3.h"
|
||||
#include "screens/ui_Settings.h"
|
||||
#include "ui_helpers.h"
|
||||
|
||||
#include "esp_log.h"
|
||||
|
||||
static const char *TAG = "ui_nav";
|
||||
|
||||
static lv_obj_t *s_nav_screens[4];
|
||||
static const char *const s_nav_names[] = {
|
||||
"Settings",
|
||||
"Screen1",
|
||||
"Screen2",
|
||||
"Screen3",
|
||||
};
|
||||
|
||||
#define NAV_SCREEN_COUNT (sizeof(s_nav_screens) / sizeof(s_nav_screens[0]))
|
||||
|
||||
static uint8_t s_nav_index;
|
||||
|
||||
void ui_nav_init(void)
|
||||
{
|
||||
s_nav_screens[0] = ui_Settings;
|
||||
s_nav_screens[1] = ui_Screen1;
|
||||
s_nav_screens[2] = ui_Screen2;
|
||||
s_nav_screens[3] = ui_Screen3;
|
||||
s_nav_index = 0;
|
||||
ESP_LOGI(TAG, "Navigation ready (%u screens, start=%s)", (unsigned)NAV_SCREEN_COUNT,
|
||||
s_nav_names[s_nav_index]);
|
||||
}
|
||||
|
||||
void ui_nav_goto(uint8_t index)
|
||||
{
|
||||
if (index >= NAV_SCREEN_COUNT) {
|
||||
return;
|
||||
}
|
||||
|
||||
lvgl_port_lock();
|
||||
s_nav_index = index;
|
||||
lv_obj_t **target = &s_nav_screens[s_nav_index];
|
||||
_ui_screen_change(target, LV_SCR_LOAD_ANIM_FADE_IN, 300, 0, NULL);
|
||||
lvgl_port_unlock();
|
||||
|
||||
ESP_LOGI(TAG, "Goto %s (index %u)", s_nav_names[s_nav_index], s_nav_index);
|
||||
}
|
||||
|
||||
void ui_nav_next(void)
|
||||
{
|
||||
lvgl_port_lock();
|
||||
s_nav_index = (s_nav_index + 1) % NAV_SCREEN_COUNT;
|
||||
lv_obj_t **target = &s_nav_screens[s_nav_index];
|
||||
_ui_screen_change(target, LV_SCR_LOAD_ANIM_FADE_IN, 300, 0, NULL);
|
||||
lvgl_port_unlock();
|
||||
|
||||
ESP_LOGI(TAG, "Next -> %s (index %u)", s_nav_names[s_nav_index], s_nav_index);
|
||||
}
|
||||
|
||||
lv_obj_t *ui_nav_current(void)
|
||||
{
|
||||
return s_nav_screens[s_nav_index];
|
||||
}
|
||||
|
||||
uint8_t ui_nav_current_index(void)
|
||||
{
|
||||
return s_nav_index;
|
||||
}
|
||||
|
||||
const char *ui_nav_current_name(void)
|
||||
{
|
||||
return s_nav_names[s_nav_index];
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
#pragma once
|
||||
|
||||
#include "lvgl.h"
|
||||
|
||||
void ui_nav_init(void);
|
||||
void ui_nav_next(void);
|
||||
void ui_nav_goto(uint8_t index);
|
||||
|
||||
lv_obj_t *ui_nav_current(void);
|
||||
uint8_t ui_nav_current_index(void);
|
||||
const char *ui_nav_current_name(void);
|
||||
Reference in New Issue
Block a user