diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt index fb133d7..f4c8161 100644 --- a/main/CMakeLists.txt +++ b/main/CMakeLists.txt @@ -1,10 +1,13 @@ idf_component_register(SRCS "app/app_main.c" "app/ui_handlers.c" + "app/ui_nav.c" + "app/motion_gesture.c" "bsp/display.c" "bsp/touch.c" "bsp/lvgl_port.c" "bsp/boot_button.c" + "bsp/imu.c" "services/wifi_manager.c" "services/ble_manager.c" "services/ble_hid.c" diff --git a/main/app/app_main.c b/main/app/app_main.c index a02eb38..baa2815 100644 --- a/main/app/app_main.c +++ b/main/app/app_main.c @@ -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(); diff --git a/main/app/motion_gesture.c b/main/app/motion_gesture.c new file mode 100644 index 0000000..3fa49b0 --- /dev/null +++ b/main/app/motion_gesture.c @@ -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 + +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); +} diff --git a/main/app/motion_gesture.h b/main/app/motion_gesture.h new file mode 100644 index 0000000..64bc12f --- /dev/null +++ b/main/app/motion_gesture.h @@ -0,0 +1,16 @@ +#pragma once + +#include + +#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); diff --git a/main/app/ui_handlers.c b/main/app/ui_handlers.c index e37ccd4..3a88229 100644 --- a/main/app/ui_handlers.c +++ b/main/app/ui_handlers.c @@ -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; diff --git a/main/app/ui_nav.c b/main/app/ui_nav.c new file mode 100644 index 0000000..be77536 --- /dev/null +++ b/main/app/ui_nav.c @@ -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]; +} diff --git a/main/app/ui_nav.h b/main/app/ui_nav.h new file mode 100644 index 0000000..1bd6861 --- /dev/null +++ b/main/app/ui_nav.h @@ -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); diff --git a/main/bsp/board_config.h b/main/bsp/board_config.h index a9e37ac..0c2e0f3 100644 --- a/main/bsp/board_config.h +++ b/main/bsp/board_config.h @@ -49,6 +49,15 @@ #define BOARD_TOUCH_I2C_FREQ_HZ 400000 #define BOARD_TOUCH_ENABLED 1 +/* IMU (QMI8658) — shares I2C bus with touch on IO47/IO48 */ +#define BOARD_IMU_I2C_NUM I2C_NUM_0 +#define BOARD_PIN_IMU_SDA 47 +#define BOARD_PIN_IMU_SCL 48 +#define BOARD_PIN_IMU_INT1 8 +#define BOARD_PIN_IMU_INT2 7 +#define BOARD_IMU_I2C_FREQ_HZ 400000 +#define BOARD_IMU_ENABLED 1 + /* User IO */ #define BOARD_PIN_BOOT 0 #define BOARD_PIN_KEY 4 diff --git a/main/bsp/imu.c b/main/bsp/imu.c new file mode 100644 index 0000000..835b48f --- /dev/null +++ b/main/bsp/imu.c @@ -0,0 +1,308 @@ +#include "imu.h" + +#include "board_config.h" +#include "touch.h" + +#include "driver/i2c_master.h" +#include "esp_log.h" +#include "esp_timer.h" +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" + +#include + +static const char *TAG = "imu"; + +#if BOARD_IMU_ENABLED + +#define QMI8658_ADDR_SDO_LOW 0x6A +#define QMI8658_ADDR_SDO_HIGH 0x6B +#define QMI8658_WHO_AM_I 0x00 +#define QMI8658_WHO_AM_I_ID 0x05 +#define QMI8658_CTRL1 0x02 +#define QMI8658_CTRL2 0x03 +#define QMI8658_CTRL3 0x04 +#define QMI8658_CTRL5 0x06 +#define QMI8658_CTRL6 0x07 +#define QMI8658_CTRL7 0x08 +#define QMI8658_RESET 0x60 +#define QMI8658_ACC_X_L 0x35 + +#define QMI8658_RESET_VALUE 0xB0 +/* Little-endian + address auto-increment (matches int16 parse below) */ +#define QMI8658_CTRL1_VALUE 0x40 +/* ±8g, 1000 Hz ODR */ +#define QMI8658_CTRL2_VALUE 0x23 +/* Gyro ±512 dps, 1000 Hz — required even when only reading accel */ +#define QMI8658_CTRL3_VALUE 0x53 +#define QMI8658_CTRL5_VALUE 0x11 +#define QMI8658_CTRL6_VALUE 0x00 +#define QMI8658_CTRL7_VALUE 0x03 + +#define QMI8658_ACCEL_SENS_8G 4096.0f + +#define IMU_SAMPLE_PERIOD_MS 10 +#define IMU_DEBUG_LOG_INTERVAL_MS 1000 + +static i2c_master_bus_handle_t s_imu_bus; +static i2c_master_dev_handle_t s_imu_dev; +static imu_sample_cb_t s_sample_cb; + +static esp_err_t imu_read_raw(int16_t *rx, int16_t *ry, int16_t *rz); + +static int16_t imu_raw16_le(const uint8_t *lo, const uint8_t *hi) +{ + return (int16_t)((*hi << 8) | *lo); +} + +static esp_err_t imu_write_u8(uint8_t reg, uint8_t val) +{ + uint8_t buf[2] = {reg, val}; + return i2c_master_transmit(s_imu_dev, buf, sizeof(buf), 100); +} + +static esp_err_t imu_read_u8(uint8_t reg, uint8_t *val) +{ + return i2c_master_transmit_receive(s_imu_dev, ®, 1, val, 1, 100); +} + +static esp_err_t imu_read_bytes(uint8_t reg, uint8_t *data, size_t len) +{ + return i2c_master_transmit_receive(s_imu_dev, ®, 1, data, len, 100); +} + +static esp_err_t imu_probe_device(uint8_t addr) +{ + i2c_device_config_t dev_cfg = { + .dev_addr_length = I2C_ADDR_BIT_LEN_7, + .device_address = addr, + .scl_speed_hz = BOARD_IMU_I2C_FREQ_HZ, + }; + + esp_err_t err = i2c_master_bus_add_device(s_imu_bus, &dev_cfg, &s_imu_dev); + if (err != ESP_OK) { + return err; + } + + uint8_t who_am_i = 0; + err = imu_read_u8(QMI8658_WHO_AM_I, &who_am_i); + if (err != ESP_OK || who_am_i != QMI8658_WHO_AM_I_ID) { + i2c_master_bus_rm_device(s_imu_dev); + s_imu_dev = NULL; + if (err == ESP_OK) { + ESP_LOGW(TAG, "WHO_AM_I mismatch at 0x%02X: 0x%02X", addr, who_am_i); + return ESP_ERR_NOT_FOUND; + } + return err; + } + + ESP_LOGI(TAG, "QMI8658 found at 0x%02X (WHO_AM_I=0x%02X)", addr, who_am_i); + return ESP_OK; +} + +static esp_err_t imu_configure(void) +{ + esp_err_t err = imu_write_u8(QMI8658_RESET, QMI8658_RESET_VALUE); + if (err != ESP_OK) { + return err; + } + vTaskDelay(pdMS_TO_TICKS(50)); + + err = imu_write_u8(QMI8658_CTRL1, QMI8658_CTRL1_VALUE); + if (err != ESP_OK) { + return err; + } + + err = imu_write_u8(QMI8658_CTRL2, QMI8658_CTRL2_VALUE); + if (err != ESP_OK) { + return err; + } + + err = imu_write_u8(QMI8658_CTRL3, QMI8658_CTRL3_VALUE); + if (err != ESP_OK) { + return err; + } + + err = imu_write_u8(QMI8658_CTRL5, QMI8658_CTRL5_VALUE); + if (err != ESP_OK) { + return err; + } + + err = imu_write_u8(QMI8658_CTRL6, QMI8658_CTRL6_VALUE); + if (err != ESP_OK) { + return err; + } + + err = imu_write_u8(QMI8658_CTRL7, QMI8658_CTRL7_VALUE); + if (err != ESP_OK) { + return err; + } + + vTaskDelay(pdMS_TO_TICKS(50)); + return ESP_OK; +} + +static void imu_log_sample(const char *label, float ax, float ay, float az, + int16_t rx, int16_t ry, int16_t rz) +{ + float mag = sqrtf(ax * ax + ay * ay + az * az); + ESP_LOGI(TAG, "%s raw=(%d,%d,%d) g=(%.3f,%.3f,%.3f) |mag|=%.3f", + label, rx, ry, rz, ax, ay, az, mag); +} + +static void imu_task(void *arg) +{ + (void)arg; + + int64_t last_log_us = esp_timer_get_time(); + + while (1) { + float ax = 0.0f; + float ay = 0.0f; + float az = 0.0f; + int16_t rx = 0; + int16_t ry = 0; + int16_t rz = 0; + + esp_err_t err = imu_read_raw(&rx, &ry, &rz); + if (err == ESP_OK) { + ax = (float)rx / QMI8658_ACCEL_SENS_8G; + ay = (float)ry / QMI8658_ACCEL_SENS_8G; + az = (float)rz / QMI8658_ACCEL_SENS_8G; + + int64_t now_us = esp_timer_get_time(); + if ((now_us - last_log_us) >= (int64_t)IMU_DEBUG_LOG_INTERVAL_MS * 1000) { + imu_log_sample("sample", ax, ay, az, rx, ry, rz); + last_log_us = now_us; + } + + if (s_sample_cb != NULL) { + s_sample_cb(ax, ay, az); + } + } + + vTaskDelay(pdMS_TO_TICKS(IMU_SAMPLE_PERIOD_MS)); + } +} + +esp_err_t imu_init(void) +{ +#if BOARD_TOUCH_ENABLED + s_imu_bus = touch_get_i2c_bus(); + if (s_imu_bus == NULL) { + ESP_LOGE(TAG, "Shared I2C bus not ready (touch not initialized?)"); + return ESP_ERR_INVALID_STATE; + } + ESP_LOGI(TAG, "Using shared I2C bus (SDA=%d SCL=%d)", BOARD_PIN_IMU_SDA, + BOARD_PIN_IMU_SCL); +#else + ESP_LOGI(TAG, "Initialize dedicated I2C for IMU"); + i2c_master_bus_config_t bus_config = { + .clk_source = I2C_CLK_SRC_DEFAULT, + .i2c_port = BOARD_IMU_I2C_NUM, + .sda_io_num = BOARD_PIN_IMU_SDA, + .scl_io_num = BOARD_PIN_IMU_SCL, + .glitch_ignore_cnt = 7, + .flags.enable_internal_pullup = true, + }; + esp_err_t bus_err = i2c_new_master_bus(&bus_config, &s_imu_bus); + if (bus_err != ESP_OK) { + return bus_err; + } +#endif + + esp_err_t err = imu_probe_device(QMI8658_ADDR_SDO_LOW); + if (err != ESP_OK) { + err = imu_probe_device(QMI8658_ADDR_SDO_HIGH); + } + if (err != ESP_OK) { + ESP_LOGE(TAG, "QMI8658 not found"); + return err; + } + + err = imu_configure(); + if (err != ESP_OK) { + ESP_LOGE(TAG, "IMU configure failed: %s", esp_err_to_name(err)); + return err; + } + + float ax = 0.0f; + float ay = 0.0f; + float az = 0.0f; + int16_t rx = 0; + int16_t ry = 0; + int16_t rz = 0; + if (imu_read_accel(&ax, &ay, &az) == ESP_OK) { + imu_read_raw(&rx, &ry, &rz); + imu_log_sample("init check", ax, ay, az, rx, ry, rz); + } else { + ESP_LOGW(TAG, "Init check read failed"); + } + + ESP_LOGI(TAG, "IMU initialized (±8g, 1000 Hz ODR)"); + return ESP_OK; +} + +static esp_err_t imu_read_raw(int16_t *rx, int16_t *ry, int16_t *rz) +{ + if (s_imu_dev == NULL || rx == NULL || ry == NULL || rz == NULL) { + return ESP_ERR_INVALID_STATE; + } + + uint8_t raw[6]; + esp_err_t err = imu_read_bytes(QMI8658_ACC_X_L, raw, sizeof(raw)); + if (err != ESP_OK) { + return err; + } + + *rx = imu_raw16_le(&raw[0], &raw[1]); + *ry = imu_raw16_le(&raw[2], &raw[3]); + *rz = imu_raw16_le(&raw[4], &raw[5]); + return ESP_OK; +} + +esp_err_t imu_read_accel(float *ax, float *ay, float *az) +{ + int16_t rx = 0; + int16_t ry = 0; + int16_t rz = 0; + + esp_err_t err = imu_read_raw(&rx, &ry, &rz); + if (err != ESP_OK) { + return err; + } + + *ax = (float)rx / QMI8658_ACCEL_SENS_8G; + *ay = (float)ry / QMI8658_ACCEL_SENS_8G; + *az = (float)rz / QMI8658_ACCEL_SENS_8G; + return ESP_OK; +} + +void imu_start_task(imu_sample_cb_t cb) +{ + s_sample_cb = cb; + xTaskCreate(imu_task, "imu_task", 3072, NULL, 4, NULL); + ESP_LOGI(TAG, "IMU sample task started"); +} + +#else + +esp_err_t imu_init(void) +{ + return ESP_OK; +} + +esp_err_t imu_read_accel(float *ax, float *ay, float *az) +{ + (void)ax; + (void)ay; + (void)az; + return ESP_ERR_NOT_SUPPORTED; +} + +void imu_start_task(imu_sample_cb_t cb) +{ + (void)cb; +} + +#endif diff --git a/main/bsp/imu.h b/main/bsp/imu.h new file mode 100644 index 0000000..2dfbe02 --- /dev/null +++ b/main/bsp/imu.h @@ -0,0 +1,9 @@ +#pragma once + +#include "esp_err.h" + +typedef void (*imu_sample_cb_t)(float ax_g, float ay_g, float az_g); + +esp_err_t imu_init(void); +esp_err_t imu_read_accel(float *ax, float *ay, float *az); +void imu_start_task(imu_sample_cb_t cb); diff --git a/main/bsp/touch.c b/main/bsp/touch.c index 80a9571..dfe7850 100644 --- a/main/bsp/touch.c +++ b/main/bsp/touch.c @@ -103,6 +103,11 @@ esp_err_t touch_init(lv_display_t *display) return ESP_OK; } +i2c_master_bus_handle_t touch_get_i2c_bus(void) +{ + return s_touch_i2c_bus; +} + #else esp_err_t touch_init(lv_display_t *display) @@ -111,4 +116,9 @@ esp_err_t touch_init(lv_display_t *display) return ESP_OK; } +i2c_master_bus_handle_t touch_get_i2c_bus(void) +{ + return NULL; +} + #endif diff --git a/main/bsp/touch.h b/main/bsp/touch.h index 8d37d06..b30d74c 100644 --- a/main/bsp/touch.h +++ b/main/bsp/touch.h @@ -1,6 +1,8 @@ #pragma once +#include "driver/i2c_master.h" #include "esp_err.h" #include "lvgl.h" esp_err_t touch_init(lv_display_t *display); +i2c_master_bus_handle_t touch_get_i2c_bus(void); diff --git a/main/ui/settings/settings_store.c b/main/ui/settings/settings_store.c index 031f490..bb48206 100644 --- a/main/ui/settings/settings_store.c +++ b/main/ui/settings/settings_store.c @@ -1,6 +1,7 @@ #include "settings_store.h" #include "display.h" +#include "motion_gesture.h" #include "esp_log.h" #include "nvs.h" @@ -15,6 +16,8 @@ static const settings_data_t SETTINGS_DEFAULTS = { .brightness_pct = 70, .volume_pct = 50, .mute = false, + .shake_nav_enabled = true, + .shake_sensitivity = SHAKE_SENS_MED, }; static settings_data_t s_data; @@ -48,6 +51,10 @@ void settings_store_load(void) load_key_u8(handle, "vol", &s_data.volume_pct, SETTINGS_DEFAULTS.volume_pct); load_key_u8(handle, "mute", &val, SETTINGS_DEFAULTS.mute); s_data.mute = val != 0; + load_key_u8(handle, "shake_en", &val, SETTINGS_DEFAULTS.shake_nav_enabled); + s_data.shake_nav_enabled = val != 0; + load_key_u8(handle, "shake_sens", &val, SETTINGS_DEFAULTS.shake_sensitivity); + s_data.shake_sensitivity = (shake_sensitivity_t)val; nvs_close(handle); ESP_LOGI(TAG, "Settings loaded (bright=%u vol=%u)", s_data.brightness_pct, @@ -83,6 +90,12 @@ esp_err_t settings_store_commit(void) if (err == ESP_OK) { err = nvs_set_u8(handle, "mute", s_data.mute ? 1 : 0); } + if (err == ESP_OK) { + err = nvs_set_u8(handle, "shake_en", s_data.shake_nav_enabled ? 1 : 0); + } + if (err == ESP_OK) { + err = nvs_set_u8(handle, "shake_sens", (uint8_t)s_data.shake_sensitivity); + } if (err == ESP_OK) { err = nvs_commit(handle); } @@ -118,4 +131,7 @@ void settings_store_apply(void) { uint16_t duty = (uint16_t)((s_data.brightness_pct * 1023U) / 100U); display_backlight_set_brightness(duty); + + motion_gesture_set_enabled(s_data.shake_nav_enabled); + motion_gesture_set_sensitivity(s_data.shake_sensitivity); } diff --git a/main/ui/settings/settings_tabs.c b/main/ui/settings/settings_tabs.c index 515302e..03f8ac7 100644 --- a/main/ui/settings/settings_tabs.c +++ b/main/ui/settings/settings_tabs.c @@ -3,6 +3,8 @@ #include "ui_handlers.h" static lv_obj_t *s_notif_switch; +static lv_obj_t *s_shake_nav_switch; +static lv_obj_t *s_shake_sens_dropdown; static lv_obj_t *s_brightness_slider; static lv_obj_t *s_brightness_pct_label; static lv_obj_t *s_volume_slider; @@ -43,6 +45,8 @@ static lv_obj_t *create_settings_body(lv_obj_t *content) void load_system_settings(lv_obj_t *content, const settings_data_t *data) { s_notif_switch = NULL; + s_shake_nav_switch = NULL; + s_shake_sens_dropdown = NULL; create_status_bar(content, "System Settings"); lv_obj_t *body = create_settings_body(content); @@ -62,6 +66,35 @@ void load_system_settings(lv_obj_t *content, const settings_data_t *data) LV_EVENT_VALUE_CHANGED, (void *)(intptr_t)SETTINGS_FIELD_NOTIFICATION); + lv_obj_t *shake_nav_label = lv_label_create(body); + lv_label_set_text(shake_nav_label, "Shake Navigation"); + lv_obj_set_pos(shake_nav_label, 4, 40); + lv_obj_set_size(shake_nav_label, 120, 28); + + s_shake_nav_switch = lv_switch_create(body); + lv_obj_set_pos(s_shake_nav_switch, 180, 40); + lv_obj_set_size(s_shake_nav_switch, 40, 28); + if (data->shake_nav_enabled) { + lv_obj_add_state(s_shake_nav_switch, LV_STATE_CHECKED); + } + lv_obj_add_event_cb(s_shake_nav_switch, settings_on_value_changed, + LV_EVENT_VALUE_CHANGED, + (void *)(intptr_t)SETTINGS_FIELD_SHAKE_NAV); + + lv_obj_t *shake_sens_label = lv_label_create(body); + lv_label_set_text(shake_sens_label, "Shake Sensitivity"); + lv_obj_set_pos(shake_sens_label, 4, 76); + lv_obj_set_size(shake_sens_label, 120, 28); + + s_shake_sens_dropdown = lv_dropdown_create(body); + lv_obj_set_pos(s_shake_sens_dropdown, 120, 76); + lv_obj_set_size(s_shake_sens_dropdown, 100, 28); + lv_dropdown_set_options(s_shake_sens_dropdown, "Low\nMed\nHigh"); + lv_dropdown_set_selected(s_shake_sens_dropdown, data->shake_sensitivity); + lv_obj_add_event_cb(s_shake_sens_dropdown, settings_on_value_changed, + LV_EVENT_VALUE_CHANGED, + (void *)(intptr_t)SETTINGS_FIELD_SHAKE_SENS); + lv_obj_t *reset_btn = lv_button_create(body); lv_obj_set_pos(reset_btn, 4, 112); lv_obj_set_size(reset_btn, 100, 28); @@ -170,6 +203,14 @@ void settings_tabs_collect(settings_tab_t tab, settings_data_t *out) out->notification_enabled = lv_obj_has_state(s_notif_switch, LV_STATE_CHECKED); } + if (s_shake_nav_switch != NULL) { + out->shake_nav_enabled = + lv_obj_has_state(s_shake_nav_switch, LV_STATE_CHECKED); + } + if (s_shake_sens_dropdown != NULL) { + out->shake_sensitivity = + (shake_sensitivity_t)lv_dropdown_get_selected(s_shake_sens_dropdown); + } break; case SETTINGS_TAB_DISPLAY: if (s_brightness_slider != NULL) { diff --git a/main/ui/settings/settings_tabs.h b/main/ui/settings/settings_tabs.h index 9fc4f9b..f330b1e 100644 --- a/main/ui/settings/settings_tabs.h +++ b/main/ui/settings/settings_tabs.h @@ -7,6 +7,8 @@ typedef enum { SETTINGS_FIELD_NOTIFICATION = 0, SETTINGS_FIELD_VOLUME, SETTINGS_FIELD_MUTE, + SETTINGS_FIELD_SHAKE_NAV, + SETTINGS_FIELD_SHAKE_SENS, } settings_field_t; void load_system_settings(lv_obj_t *content, const settings_data_t *data); diff --git a/main/ui/settings/settings_types.h b/main/ui/settings/settings_types.h index df036ad..a8c8d96 100644 --- a/main/ui/settings/settings_types.h +++ b/main/ui/settings/settings_types.h @@ -10,9 +10,17 @@ typedef enum { SETTINGS_TAB_NETWORK, } settings_tab_t; +typedef enum { + SHAKE_SENS_LOW = 0, + SHAKE_SENS_MED, + SHAKE_SENS_HIGH, +} shake_sensitivity_t; + typedef struct { bool notification_enabled; uint8_t brightness_pct; uint8_t volume_pct; bool mute; + bool shake_nav_enabled; + shake_sensitivity_t shake_sensitivity; } settings_data_t;