diff --git a/.github/workflows/build-firmware.yml b/.github/workflows/build-firmware.yml index cba0d62f7..005b973fc 100644 --- a/.github/workflows/build-firmware.yml +++ b/.github/workflows/build-firmware.yml @@ -26,7 +26,7 @@ jobs: strategy: matrix: - board: ["snowy_bb2", "spalding_bb2", "silk_bb2", "asterix", "obelix_evt", "obelix_dvt"] + board: ["snowy_bb2", "spalding_bb2", "silk_bb2", "asterix", "obelix_evt", "obelix_dvt", "obelix_pvt"] steps: - name: Mark Github workspace as safe diff --git a/.github/workflows/build-prf.yml b/.github/workflows/build-prf.yml index 0fbc62dd8..06ce7bcfd 100644 --- a/.github/workflows/build-prf.yml +++ b/.github/workflows/build-prf.yml @@ -24,7 +24,7 @@ jobs: strategy: matrix: - board: ["asterix", "obelix_evt", "obelix_dvt"] + board: ["asterix", "obelix_evt", "obelix_dvt", "obelix_pvt"] mode: ["normal", "mfg"] steps: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 90c89794f..18f005694 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -87,7 +87,7 @@ jobs: strategy: matrix: - board: ["asterix", "obelix_evt", "obelix_dvt"] + board: ["asterix", "obelix_evt", "obelix_dvt", "obelix_pvt"] steps: - name: Mark Github workspace as safe @@ -169,7 +169,7 @@ jobs: strategy: matrix: - board: ["asterix", "obelix_evt", "obelix_dvt"] + board: ["asterix", "obelix_evt", "obelix_dvt", "obelix_pvt"] slot: [0, 1] exclude: @@ -211,7 +211,7 @@ jobs: - name: Set slot suffix id: slot_suffix run: | - NEEDS_SUFFIX="obelix_evt obelix_dvt" + NEEDS_SUFFIX="obelix_evt obelix_dvt obelix_pvt" if echo $NEEDS_SUFFIX | grep -wq "${{ matrix.board }}"; then echo "SLOT_SUFFIX=_slot${{ matrix.slot }}" >> "$GITHUB_OUTPUT" @@ -292,7 +292,7 @@ jobs: - name: Merge slot-specific pbz files run: | - NEEDS_MERGE="obelix_evt obelix_dvt" + NEEDS_MERGE="obelix_evt obelix_dvt obelix_pvt" mkdir artifacts-merged for board in $NEEDS_MERGE; do diff --git a/.gitmodules b/.gitmodules index a2cf9578e..81ad07773 100644 --- a/.gitmodules +++ b/.gitmodules @@ -40,3 +40,6 @@ [submodule "third_party/hal_lsm6dso/lsm6dso-pid"] path = third_party/hal_lsm6dso/lsm6dso-pid url = https://github.com/STMicroelectronics/lsm6dso-pid.git +[submodule "third_party/hal_lis2dw12/lis2dw12-pid"] + path = third_party/hal_lis2dw12/lis2dw12-pid + url = https://github.com/STMicroelectronics/lis2dw12-pid.git diff --git a/platform/platform_capabilities.py b/platform/platform_capabilities.py index 72c1b3e53..ea362d0b5 100644 --- a/platform/platform_capabilities.py +++ b/platform/platform_capabilities.py @@ -331,7 +331,7 @@ }, }, { - 'boards': [ 'obelix_evt', 'obelix_dvt', 'obelix_bb', 'obelix_bb2' ], + 'boards': [ 'obelix_evt', 'obelix_dvt', 'obelix_pvt', 'obelix_bb', 'obelix_bb2' ], 'capabilities': { 'HAS_APP_GLANCES', diff --git a/platform/wscript b/platform/wscript index b58236532..f69083069 100644 --- a/platform/wscript +++ b/platform/wscript @@ -47,13 +47,25 @@ def is_asterix(ctx): def is_obelix(ctx): return ctx.get_board().startswith('obelix') +@conf +def is_obelix_evt(ctx): + return ctx.get_board() == 'obelix_evt' + +@conf +def is_obelix_bb(ctx): + return ctx.get_board() == 'obelix_bb' + @conf def is_obelix_dvt(ctx): - return ctx.get_board().startswith('obelix_dvt') + return ctx.get_board() == 'obelix_dvt' + +@conf +def is_obelix_pvt(ctx): + return ctx.get_board() == 'obelix_pvt' @conf def is_obelix_bb2(ctx): - return ctx.get_board().startswith('obelix_bb2') + return ctx.get_board() == 'obelix_bb2' @conf def get_platform_name(ctx): diff --git a/src/fw/apps/prf_apps/mfg_menu_app.c b/src/fw/apps/prf_apps/mfg_menu_app.c index 5af428823..52447f36b 100644 --- a/src/fw/apps/prf_apps/mfg_menu_app.c +++ b/src/fw/apps/prf_apps/mfg_menu_app.c @@ -78,7 +78,6 @@ static void prv_launch_app_cb(void *data) { app_manager_launch_new_app(&(AppLaunchConfig) { .md = data }); } -#ifdef MANUFACTURING_FW static void prv_select_bt_device_name(int index, void *context) { launcher_task_add_callback(prv_launch_app_cb, (void*) mfg_bt_device_name_app_get_info()); } @@ -187,7 +186,6 @@ static void prv_select_load_prf(int index, void *context) { #endif system_reset(); } -#endif // MANUFACTURING_FW static void prv_select_reset(int index, void *context) { system_reset(); @@ -239,7 +237,6 @@ static size_t prv_create_menu_items(SimpleMenuItem** out_menu_items) { // Define a const blueprint on the stack. const SimpleMenuItem s_menu_items[] = { -#ifdef MANUFACTURING_FW { .title = "BT Device Name", .callback = prv_select_bt_device_name }, { .title = "Device Serial", .callback = prv_select_serial_qr }, #if PBL_ROUND @@ -280,7 +277,6 @@ static size_t prv_create_menu_items(SimpleMenuItem** out_menu_items) { { .title = "Certification", .callback = prv_select_certification }, { .title = "Program Color", .callback = prv_select_program_color }, { .title = "Load PRF", .callback = prv_select_load_prf }, -#endif { .title = "Reset", .callback = prv_select_reset }, { .title = "Shutdown", .callback = prv_select_shutdown } }; @@ -289,7 +285,6 @@ static size_t prv_create_menu_items(SimpleMenuItem** out_menu_items) { *out_menu_items = app_malloc(sizeof(s_menu_items)); memcpy(*out_menu_items, s_menu_items, sizeof(s_menu_items)); -#if MANUFACTURING_FW // Now we're going to modify the first two elements in the menu to include data available only // at runtime. If it was available at compile time we could have just shoved it in the // s_menu_items array but it's not. Note that we allocate a few buffers here that we never @@ -309,7 +304,6 @@ static size_t prv_create_menu_items(SimpleMenuItem** out_menu_items) { mfg_info_get_serialnumber(device_serial, buffer_size); (*out_menu_items)[1].subtitle = device_serial; -#endif // We've now populated out_menu_items with the correct data. Return the number of items by // looking at the original list of menu items. diff --git a/src/fw/apps/system_apps/settings/settings_display.c b/src/fw/apps/system_apps/settings/settings_display.c index 5af126467..b8e776369 100644 --- a/src/fw/apps/system_apps/settings/settings_display.c +++ b/src/fw/apps/system_apps/settings/settings_display.c @@ -43,6 +43,9 @@ typedef struct SettingsDisplayData { SettingsCallbacks callbacks; + char als_value_buffer[16]; // Buffer for ALS value display + char backlight_percent_buffer[16]; // Buffer for backlight percentage display + AppTimer *update_timer; // Timer for live updating debug values } SettingsDisplayData; // Intensity Settings @@ -95,6 +98,43 @@ static void prv_intensity_menu_push(SettingsDisplayData *data) { true /* icons_enabled */, s_intensity_labels, data); } +#if PLATFORM_ASTERIX +// Orientation Settings +///////////////////////////// +static const char *s_display_orientation_labels[] = { + i18n_noop("Default"), + i18n_noop("Left-Handed"), +}; + +static int prv_display_orientation_get_selection_index() { + return display_orientation_is_left() ? 1 : 0; +} + +static void prv_display_orientation_menu_select(OptionMenu *option_menu, int selection, + void *context) { + if (prv_display_orientation_get_selection_index() == selection) { + // No change + app_window_stack_remove(&option_menu->window, true /* animated */); + return; + } + + display_orientation_set_left(!display_orientation_is_left()); + app_window_stack_remove(&option_menu->window, true /* animated */); +} + +static void prv_display_orientation_menu_push(SettingsDisplayData *data) { + const int index = prv_display_orientation_get_selection_index(); + const OptionMenuCallbacks callbacks = { + .select = prv_display_orientation_menu_select, + }; + + const char *title = i18n_noop("Orientation"); + settings_option_menu_push(title, OptionMenuContentType_SingleLine, index, &callbacks, + ARRAY_LENGTH(s_display_orientation_labels), true, + s_display_orientation_labels, data); +} +#endif + // Timeout Settings ///////////////////////////// @@ -163,6 +203,9 @@ static void prv_legacy_app_mode_menu_push(SettingsDisplayData *data) { enum SettingsDisplayItem { SettingsDisplayLanguage, +#if PLATFORM_ASTERIX + SettingsDisplayOrientation, +#endif SettingsDisplayBacklightMode, SettingsDisplayMotionSensor, SettingsDisplayAmbientSensor, @@ -202,6 +245,11 @@ static void prv_select_click_cb(SettingsCallbacks *context, uint16_t row) { case SettingsDisplayLanguage: shell_prefs_toggle_language_english(); break; +#if PLATFORM_ASTERIX + case SettingsDisplayOrientation: + prv_display_orientation_menu_push(data); + break; +#endif case SettingsDisplayBacklightMode: light_toggle_enabled(); break; @@ -250,10 +298,28 @@ static void prv_draw_row_cb(SettingsCallbacks *context, GContext *ctx, title = i18n_noop("Language"); subtitle = i18n_get_lang_name(); break; +#if PLATFORM_ASTERIX + case SettingsDisplayOrientation: + title = i18n_noop("Orientation"); + subtitle = s_display_orientation_labels[prv_display_orientation_get_selection_index()]; + break; +#endif case SettingsDisplayBacklightMode: title = i18n_noop("Backlight"); if (backlight_is_enabled()) { +#if CAPABILITY_HAS_DYNAMIC_BACKLIGHT + // Show current backlight percentage when dynamic backlight is enabled + if (backlight_is_dynamic_intensity_enabled()) { + uint8_t current_percent = light_get_current_brightness_percent(); + snprintf(data->backlight_percent_buffer, sizeof(data->backlight_percent_buffer), + "On - %"PRIu8"%%", current_percent); + subtitle = data->backlight_percent_buffer; + } else { + subtitle = i18n_noop("On"); + } +#else subtitle = i18n_noop("On"); +#endif } else { subtitle = i18n_noop("Off"); } @@ -269,7 +335,11 @@ static void prv_draw_row_cb(SettingsCallbacks *context, GContext *ctx, case SettingsDisplayAmbientSensor: title = i18n_noop("Ambient Sensor"); if (backlight_is_ambient_sensor_enabled()) { - subtitle = i18n_noop("On"); + // Display ALS value when ambient sensor is enabled + uint32_t als_value = ambient_light_get_light_level(); + snprintf(data->als_value_buffer, sizeof(data->als_value_buffer), + "On (%"PRIu32")", als_value); + subtitle = data->als_value_buffer; } else { subtitle = i18n_noop("Off"); } @@ -330,10 +400,41 @@ static uint16_t prv_num_rows_cb(SettingsCallbacks *context) { static void prv_deinit_cb(SettingsCallbacks *context) { SettingsDisplayData *data = (SettingsDisplayData*) context; + if (data->update_timer) { + app_timer_cancel(data->update_timer); + data->update_timer = NULL; + } i18n_free_all(data); app_free(data); } +// Timer callback to update debug values +#define UPDATE_INTERVAL_MS 500 // Update twice per second +static void prv_update_timer_cb(void *context) { + SettingsDisplayData *data = (SettingsDisplayData*)context; + + // Mark the menu as dirty to trigger a redraw + settings_menu_mark_dirty(SettingsMenuItemDisplay); + + // Reschedule the timer + data->update_timer = app_timer_register(UPDATE_INTERVAL_MS, prv_update_timer_cb, data); +} + +static void prv_appear_cb(SettingsCallbacks *context) { + SettingsDisplayData *data = (SettingsDisplayData*)context; + if (!data->update_timer) { + data->update_timer = app_timer_register(UPDATE_INTERVAL_MS, prv_update_timer_cb, data); + } +} + +static void prv_hide_cb(SettingsCallbacks *context) { + SettingsDisplayData *data = (SettingsDisplayData*)context; + if (data->update_timer) { + app_timer_cancel(data->update_timer); + data->update_timer = NULL; + } +} + static Window *prv_init(void) { SettingsDisplayData *data = app_malloc_check(sizeof(*data)); *data = (SettingsDisplayData){}; @@ -343,6 +444,8 @@ static Window *prv_init(void) { .draw_row = prv_draw_row_cb, .select_click = prv_select_click_cb, .num_rows = prv_num_rows_cb, + .appear = prv_appear_cb, + .hide = prv_hide_cb, }; return settings_window_create(SettingsMenuItemDisplay, &data->callbacks); diff --git a/src/fw/apps/system_apps/settings/settings_system.c b/src/fw/apps/system_apps/settings/settings_system.c index 2ee91dcf4..fbe4d32a4 100644 --- a/src/fw/apps/system_apps/settings/settings_system.c +++ b/src/fw/apps/system_apps/settings/settings_system.c @@ -76,8 +76,12 @@ enum { DebuggingItemCoreDumpNow = 0, DebuggingItemCoreDumpShortcut, DebuggingItemALSThreshold, -#if PLATFORM_ASTERIX +#if PLATFORM_ASTERIX || PLATFORM_OBELIX DebuggingItemMotionSensitivity, +#endif +#if CAPABILITY_HAS_DYNAMIC_BACKLIGHT + DebuggingItemDynamicBacklightMinThreshold, + DebuggingItemDynamicBacklightMaxThreshold, #endif DebuggingItem_Count, }; @@ -148,6 +152,12 @@ typedef struct SettingsSystemData { char als_threshold_buffer[16]; // Buffer for formatted ALS threshold char als_status_buffer[64]; // Buffer for NumberWindow label with status bool als_adjustment_active; // Track if ALS adjustment is active + +#if CAPABILITY_HAS_DYNAMIC_BACKLIGHT + // Dynamic backlight threshold data + char dyn_bl_min_threshold_buffer[16]; // Buffer for formatted min threshold + char dyn_bl_max_threshold_buffer[16]; // Buffer for formatted max threshold +#endif } SettingsSystemData; typedef enum { @@ -468,9 +478,73 @@ static void prv_als_threshold_menu_push(SettingsSystemData *data) { app_window_stack_push(&number_window->window, animated); } +// Dynamic Backlight Min Threshold Settings +///////////////////////////// +#if CAPABILITY_HAS_DYNAMIC_BACKLIGHT +static void prv_dyn_bl_min_threshold_selected(NumberWindow *number_window, void *context) { + uint32_t new_threshold = (uint32_t)number_window_get_value(number_window); + backlight_set_dynamic_min_threshold(new_threshold); + app_window_stack_remove(&number_window->window, true /* animated */); +} + +static void prv_dyn_bl_min_threshold_menu_push(SettingsSystemData *data) { + NumberWindow *number_window = number_window_create( + "Min Light Threshold", + (NumberWindowCallbacks) { + .selected = prv_dyn_bl_min_threshold_selected, + }, + data + ); + + if (!number_window) { + return; + } + + // Set reasonable min/max values + number_window_set_min(number_window, 0); + number_window_set_max(number_window, AMBIENT_LIGHT_LEVEL_MAX); + number_window_set_step_size(number_window, 1); + number_window_set_value(number_window, (int32_t)backlight_get_dynamic_min_threshold()); + + const bool animated = true; + app_window_stack_push(&number_window->window, animated); +} + +// Dynamic Backlight Max Threshold Settings +///////////////////////////// +static void prv_dyn_bl_max_threshold_selected(NumberWindow *number_window, void *context) { + uint32_t new_threshold = (uint32_t)number_window_get_value(number_window); + backlight_set_dynamic_max_threshold(new_threshold); + app_window_stack_remove(&number_window->window, true /* animated */); +} + +static void prv_dyn_bl_max_threshold_menu_push(SettingsSystemData *data) { + NumberWindow *number_window = number_window_create( + "Max Light Threshold", + (NumberWindowCallbacks) { + .selected = prv_dyn_bl_max_threshold_selected, + }, + data + ); + + if (!number_window) { + return; + } + + // Set reasonable min/max values + number_window_set_min(number_window, 1); + number_window_set_max(number_window, AMBIENT_LIGHT_LEVEL_MAX); + number_window_set_step_size(number_window, 1); + number_window_set_value(number_window, (int32_t)backlight_get_dynamic_max_threshold()); + + const bool animated = true; + app_window_stack_push(&number_window->window, animated); +} +#endif + // Motion Sensitivity Settings (Asterix/Obelix only) ///////////////////////////// -#if PLATFORM_ASTERIX +#if PLATFORM_ASTERIX || PLATFORM_OBELIX static const uint8_t s_motion_sensitivity_values[] = { 10, 25, 40, 55, 70, 85, 100 }; static const char *s_motion_sensitivity_labels[] = { @@ -520,9 +594,13 @@ static const char* s_debugging_titles[DebuggingItem_Count] = { [DebuggingItemCoreDumpNow] = i18n_noop("CoreDump now"), [DebuggingItemCoreDumpShortcut] = i18n_noop("CoreDump shortcut"), [DebuggingItemALSThreshold] = i18n_noop("ALS Threshold"), -#if PLATFORM_ASTERIX +#if PLATFORM_ASTERIX || PLATFORM_OBELIX [DebuggingItemMotionSensitivity] = i18n_noop("Motion Sensitivity"), #endif +#if CAPABILITY_HAS_DYNAMIC_BACKLIGHT + [DebuggingItemDynamicBacklightMinThreshold] = i18n_noop("Dyn BL Min Threshold"), + [DebuggingItemDynamicBacklightMaxThreshold] = i18n_noop("Dyn BL Max Threshold"), +#endif }; static void prv_debugging_draw_row_callback(GContext* ctx, const Layer *cell_layer, @@ -549,10 +627,24 @@ static void prv_debugging_draw_row_callback(GContext* ctx, const Layer *cell_lay "%"PRIu32, current_threshold); subtitle_text = data->als_threshold_buffer; } -#if PLATFORM_ASTERIX +#if PLATFORM_ASTERIX || PLATFORM_OBELIX else if (cell_index->row == DebuggingItemMotionSensitivity) { subtitle_text = i18n_get(s_motion_sensitivity_labels[prv_motion_sensitivity_get_selection_index()], data); } +#endif +#if CAPABILITY_HAS_DYNAMIC_BACKLIGHT + else if (cell_index->row == DebuggingItemDynamicBacklightMinThreshold) { + uint32_t min_threshold = backlight_get_dynamic_min_threshold(); + snprintf(data->dyn_bl_min_threshold_buffer, sizeof(data->dyn_bl_min_threshold_buffer), + "%"PRIu32, min_threshold); + subtitle_text = data->dyn_bl_min_threshold_buffer; + } + else if (cell_index->row == DebuggingItemDynamicBacklightMaxThreshold) { + uint32_t max_threshold = backlight_get_dynamic_max_threshold(); + snprintf(data->dyn_bl_max_threshold_buffer, sizeof(data->dyn_bl_max_threshold_buffer), + "%"PRIu32, max_threshold); + subtitle_text = data->dyn_bl_max_threshold_buffer; + } #endif menu_cell_basic_draw(ctx, cell_layer, title, subtitle_text, NULL); } @@ -585,10 +677,18 @@ static void prv_debugging_select_callback(MenuLayer *menu_layer, case DebuggingItemALSThreshold: prv_als_threshold_menu_push(data); break; -#if PLATFORM_ASTERIX +#if PLATFORM_ASTERIX || PLATFORM_OBELIX case DebuggingItemMotionSensitivity: prv_motion_sensitivity_menu_push(data); break; +#endif +#if CAPABILITY_HAS_DYNAMIC_BACKLIGHT + case DebuggingItemDynamicBacklightMinThreshold: + prv_dyn_bl_min_threshold_menu_push(data); + break; + case DebuggingItemDynamicBacklightMaxThreshold: + prv_dyn_bl_max_threshold_menu_push(data); + break; #endif default: WTF; diff --git a/src/fw/board/board_definitions.h b/src/fw/board/board_definitions.h index c204bc03b..f786763ba 100644 --- a/src/fw/board/board_definitions.h +++ b/src/fw/board/board_definitions.h @@ -65,7 +65,7 @@ #include "boards/board_robert.h" // prototypes for Robert EVT #elif BOARD_ASTERIX #include "boards/board_asterix.h" -#elif BOARD_OBELIX_EVT || BOARD_OBELIX_DVT || BOARD_OBELIX_BB || BOARD_OBELIX_BB2 +#elif BOARD_OBELIX_EVT || BOARD_OBELIX_DVT || BOARD_OBELIX_PVT || BOARD_OBELIX_BB || BOARD_OBELIX_BB2 #include "boards/board_obelix.h" #else #error "Unknown board definition" diff --git a/src/fw/board/boards/board_obelix.c b/src/fw/board/boards/board_obelix.c index 08c6a8183..af434e2ad 100644 --- a/src/fw/board/boards/board_obelix.c +++ b/src/fw/board/boards/board_obelix.c @@ -25,7 +25,6 @@ #include "drivers/sf32lb52/debounced_button_definitions.h" #include "drivers/stubs/hrm.h" #include "system/passert.h" -#include "kernel/util/stop.h" #define HCPU_FREQ_MHZ 240 @@ -116,7 +115,7 @@ static PwmState s_pwm1_ch3_state = { .channel = 3, }; -#if !BOARD_OBELIX_DVT && !BOARD_OBELIX_BB2 +#if !BOARD_OBELIX_DVT && !BOARD_OBELIX_PVT && !BOARD_OBELIX_BB2 const LedControllerPwm LED_CONTROLLER_PWM = { .pwm = { [0] = { @@ -234,7 +233,7 @@ static DisplayJDIDevice s_display = { .flags = PIN_NOPULL, }, }, -#if BOARD_OBELIX_DVT || BOARD_OBELIX_BB2 +#if BOARD_OBELIX_DVT || BOARD_OBELIX_PVT || BOARD_OBELIX_BB2 .vddp = {hwp_gpio1, 28, true}, .vlcd = {hwp_gpio1, 29, true}, #endif @@ -410,12 +409,25 @@ I2CBus *const I2C2_BUS = &s_i2c_bus_2; IRQ_MAP(I2C2, i2c_irq_handler, I2C2_BUS); +#ifdef IMU_USE_LIS2DW12 +static const I2CSlavePort s_i2c_lsm2dw12 = { + .bus = &s_i2c_bus_2, +#if BOARD_OBELIX_DVT + .address = 0x19, +#else + .address = 0x18, +#endif +}; + +I2CSlavePort *const I2C_LSM2DW12 = &s_i2c_lsm2dw12; +#else static const I2CSlavePort s_i2c_lsm6d = { .bus = &s_i2c_bus_2, .address = 0x6A, }; I2CSlavePort *const I2C_LSM6D = &s_i2c_lsm6d; +#endif static const I2CSlavePort s_i2c_mmc5603nj = { .bus = &s_i2c_bus_2, @@ -686,7 +698,4 @@ void board_init(void) { HAL_HPAON_EnableWakeupSrc(HPAON_WAKEUP_SRC_PIN11, AON_PIN_MODE_LOW); HAL_HPAON_EnableWakeupSrc(HPAON_WAKEUP_SRC_PIN12, AON_PIN_MODE_LOW); HAL_HPAON_EnableWakeupSrc(HPAON_WAKEUP_SRC_PIN13, AON_PIN_MODE_LOW); - - // Temporarily disable stop mode (GH-452) - stop_mode_disable(InhibitorMain); } diff --git a/src/fw/board/boards/board_obelix.h b/src/fw/board/boards/board_obelix.h index df8856e83..1458eba52 100644 --- a/src/fw/board/boards/board_obelix.h +++ b/src/fw/board/boards/board_obelix.h @@ -32,7 +32,11 @@ extern QSPIPort * const QSPI; extern QSPIFlash * const QSPI_FLASH; extern I2CBus *const I2C1_BUS; extern I2CBus *const I2C2_BUS; +#ifdef IMU_USE_LIS2DW12 +extern I2CSlavePort *const I2C_LSM2DW12; +#else extern I2CSlavePort * const I2C_LSM6D; +#endif extern I2CSlavePort * const I2C_MMC5603NJ; extern I2CSlavePort * const I2C_NPM1300; extern I2CSlavePort *const I2C_AW86225; @@ -40,7 +44,7 @@ extern I2CSlavePort *const I2C_W1160; extern I2CSlavePort *const I2C_AW2016; extern const Npm1300Config NPM1300_CONFIG; extern const BoardConfigActuator BOARD_CONFIG_VIBE; -#if !BOARD_OBELIX_DVT && !BOARD_OBELIX_BB2 +#if !BOARD_OBELIX_DVT && !BOARD_OBELIX_PVT && !BOARD_OBELIX_BB2 extern const LedControllerPwm LED_CONTROLLER_PWM; #endif extern PwmConfig *const PWM1_CH1; diff --git a/src/fw/board/display.h b/src/fw/board/display.h index 3d9f6db9c..45a8f4362 100644 --- a/src/fw/board/display.h +++ b/src/fw/board/display.h @@ -79,7 +79,7 @@ typedef struct { #include "displays/display_silk.h" #elif BOARD_ASTERIX #include "displays/display_silk.h" -#elif BOARD_OBELIX_EVT || BOARD_OBELIX_DVT || BOARD_OBELIX_BB || BOARD_OBELIX_BB2 +#elif BOARD_OBELIX_EVT || BOARD_OBELIX_DVT || BOARD_OBELIX_PVT || BOARD_OBELIX_BB || BOARD_OBELIX_BB2 #include "displays/display_obelix.h" #elif BOARD_CUTTS_BB #include "displays/display_snowy.h" diff --git a/src/fw/board/splash.h b/src/fw/board/splash.h index 64907f35b..abc786f54 100644 --- a/src/fw/board/splash.h +++ b/src/fw/board/splash.h @@ -16,7 +16,7 @@ #pragma once -#if BOARD_OBELIX_EVT || BOARD_OBELIX_DVT || BOARD_OBELIX_BB || BOARD_OBELIX_BB2 +#if BOARD_OBELIX_EVT || BOARD_OBELIX_DVT || BOARD_OBELIX_PVT || BOARD_OBELIX_BB || BOARD_OBELIX_BB2 #include "splash/splash_obelix.xbm" #else #error "Unknown splash definition for board" diff --git a/src/fw/board/wscript_build b/src/fw/board/wscript_build index 6f55f271d..65d60aca0 100644 --- a/src/fw/board/wscript_build +++ b/src/fw/board/wscript_build @@ -99,7 +99,7 @@ elif board in ('asterix'): 'drivers', ], ) -elif board in ('obelix_evt', 'obelix_dvt', 'obelix_bb', 'obelix_bb2'): +elif board in ('obelix_evt', 'obelix_dvt', 'obelix_pvt', 'obelix_bb', 'obelix_bb2'): bld.objects( name='board', source=[ diff --git a/src/fw/drivers/accel.h b/src/fw/drivers/accel.h index dd9fc8e11..464361c59 100644 --- a/src/fw/drivers/accel.h +++ b/src/fw/drivers/accel.h @@ -208,3 +208,8 @@ extern bool accel_run_selftest(void); //! threshold, where any minor amount of motion would trigger the system shake event. //! Note: Setting this value does not ensure that shake detection is enabled. void accel_set_shake_sensitivity_high(bool sensitivity_high); + + +//! Update the accelerometer shake sensitivity as a percentage value from 0 to 100. +//! Note: Setting this value does not ensure that shake detection is enabled. +void accel_set_shake_sensitivity_percent(uint8_t percent); diff --git a/src/fw/drivers/button.h b/src/fw/drivers/button.h index 98ede8bb8..bbd91d5ca 100644 --- a/src/fw/drivers/button.h +++ b/src/fw/drivers/button.h @@ -23,6 +23,8 @@ void button_init(void); +void button_set_rotated(bool rotated); + bool button_is_pressed(ButtonId id); uint8_t button_get_state_bits(void); diff --git a/src/fw/drivers/display/display.h b/src/fw/drivers/display/display.h index 30abf9bdf..336618c3f 100644 --- a/src/fw/drivers/display/display.h +++ b/src/fw/drivers/display/display.h @@ -41,6 +41,8 @@ void display_clear(void); void display_set_enabled(bool enabled); +void display_set_rotated(bool rotated); + void display_update(NextRowCallback nrcb, UpdateCompleteCallback uccb); bool display_update_in_progress(void); diff --git a/src/fw/drivers/display/sharp_ls013b7dh01/sharp_ls013b7dh01_nrf5.c b/src/fw/drivers/display/sharp_ls013b7dh01/sharp_ls013b7dh01_nrf5.c index c0a8cc534..70f2f3555 100644 --- a/src/fw/drivers/display/sharp_ls013b7dh01/sharp_ls013b7dh01_nrf5.c +++ b/src/fw/drivers/display/sharp_ls013b7dh01/sharp_ls013b7dh01_nrf5.c @@ -27,6 +27,8 @@ #include "kernel/util/stop.h" #include "os/mutex.h" #include "system/passert.h" +#include "util/reverse.h" + #include #include #include @@ -45,6 +47,9 @@ static bool s_updating; static UpdateCompleteCallback s_uccb; static SemaphoreHandle_t s_sem; +// watch rotation +static bool s_rotated_180 = false; + static void prv_extcomin_init(void) { nrfx_err_t err; const NrfLowPowerPWM *extcomin = &BOARD_CONFIG_DISPLAY.extcomin; @@ -188,6 +193,10 @@ void display_set_enabled(bool enabled) { gpio_output_set(&BOARD_CONFIG_DISPLAY.on_ctrl, enabled); } +void display_set_rotated(bool rotated) { + s_rotated_180 = rotated; +} + void display_update(NextRowCallback nrcb, UpdateCompleteCallback uccb) { DisplayRow row; uint8_t *pbuf = s_buf; @@ -201,11 +210,16 @@ void display_update(NextRowCallback nrcb, UpdateCompleteCallback uccb) { while (nrcb(&row)) { // write row address, data and trailing dummy - *pbuf++ = row.address + 1; - memcpy(pbuf, row.data, DISP_LINE_BYTES); - pbuf += DISP_LINE_BYTES; + *pbuf++ = s_rotated_180 ? (PBL_DISPLAY_HEIGHT - 1) - row.address + 1 : row.address + 1; + if (s_rotated_180) { + for (int i = DISP_LINE_BYTES - 1; i >= 0; --i) { + *pbuf++ = reverse_byte(row.data[i]); + } + } else { + memcpy(pbuf, row.data, DISP_LINE_BYTES); + pbuf += DISP_LINE_BYTES; + } *pbuf++ = 0x00; - desc.tx_length += DISP_LINE_BYTES + 2; } diff --git a/src/fw/drivers/imu/bma255/bma255.c b/src/fw/drivers/imu/bma255/bma255.c index 94413179c..7ae044432 100644 --- a/src/fw/drivers/imu/bma255/bma255.c +++ b/src/fw/drivers/imu/bma255/bma255.c @@ -701,6 +701,23 @@ void accel_set_shake_sensitivity_high(bool sensitivity_high) { } } +void accel_set_shake_sensitivity_percent(uint8_t percent) { + // Map percent [0,100] to threshold [high, low] + const uint8_t high_threshold = + BOARD_CONFIG_ACCEL.accel_config.shake_thresholds[AccelThresholdHigh]; + const uint8_t low_threshold = + BOARD_CONFIG_ACCEL.accel_config.shake_thresholds[AccelThresholdLow]; + + if (percent >= 100) { + accel_set_shake_sensitivity_high(false /* sensitivity_high */); + return; + } + + const uint8_t threshold = high_threshold + + ((low_threshold - high_threshold) * (100 - percent)) / 100; + bma255_write_register(BMA255Register_INT_6, threshold); +} + bool accel_get_shake_detection_enabled(void) { return s_shake_detection_enabled; } diff --git a/src/fw/drivers/imu/bmi160/bmi160.c b/src/fw/drivers/imu/bmi160/bmi160.c index 8d9c1fef1..07c1f131b 100644 --- a/src/fw/drivers/imu/bmi160/bmi160.c +++ b/src/fw/drivers/imu/bmi160/bmi160.c @@ -30,6 +30,7 @@ #include #include +#include #include // Note: Before adding a new header, be sure you actually need it! The goal @@ -944,6 +945,23 @@ void accel_set_shake_sensitivity_high(bool sensitivity_high) { } } +void accel_set_shake_sensitivity_percent(uint8_t percent) { + // Map percent [0,100] to threshold [high, low] + const uint8_t high_threshold = + BOARD_CONFIG_ACCEL.accel_config.shake_thresholds[AccelThresholdHigh]; + const uint8_t low_threshold = + BOARD_CONFIG_ACCEL.accel_config.shake_thresholds[AccelThresholdLow]; + + if (percent >= 100) { + accel_set_shake_sensitivity_high(false /* sensitivity_high */); + return; + } + + const uint8_t threshold = high_threshold + + ((low_threshold - high_threshold) * (100 - percent)) / 100; + prv_write_reg(BMI160_REG_INT_MOTION_1, threshold); +} + static void prv_enable_shake_detection(void) { // don't automatically power-up the gryo when an anymotion interrupt fires! diff --git a/src/fw/drivers/imu/imu_obelix.c b/src/fw/drivers/imu/imu_obelix.c index 39131c567..db73bdeca 100644 --- a/src/fw/drivers/imu/imu_obelix.c +++ b/src/fw/drivers/imu/imu_obelix.c @@ -16,17 +16,30 @@ #include "drivers/imu.h" #include "drivers/imu/lsm6dso/lsm6dso.h" +#include "drivers/imu/lis2dw12/lis2dw12.h" #include "drivers/imu/mmc5603nj/mmc5603nj.h" void imu_init(void) { +#ifdef IMU_USE_LIS2DW12 + lis2dw12_init(); +#else lsm6dso_init(); +#endif mmc5603nj_init(); } void imu_power_up(void) { +#ifdef IMU_USE_LIS2DW12 + lis2dw12_power_up(); +#else lsm6dso_power_up(); +#endif } void imu_power_down(void) { +#ifdef IMU_USE_LIS2DW12 + lis2dw12_power_down(); +#else lsm6dso_power_down(); +#endif } diff --git a/src/fw/drivers/imu/lis2dw12/lis2dw12.c b/src/fw/drivers/imu/lis2dw12/lis2dw12.c new file mode 100644 index 000000000..f51dd33c9 --- /dev/null +++ b/src/fw/drivers/imu/lis2dw12/lis2dw12.c @@ -0,0 +1,870 @@ +/* + * Copyright 2025 Core Devices LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "board/board.h" +#include "drivers/accel.h" +#include "drivers/i2c.h" +#include "drivers/exti.h" +#include "kernel/util/sleep.h" +#include "system/logging.h" +#include "drivers/vibe.h" +#include "services/common/vibe_pattern.h" +#include "system/passert.h" +#include "lis2dw12_reg.h" + +// Error recovery thresholds and watchdog timeouts +#define LIS2DW12_MAX_CONSECUTIVE_FAILURES 3 +#define LIS2DW12_INTERRUPT_GAP_LOG_THRESHOLD_MS 3000 +#define LIS2DW12_FIFO_MAX_WATERMARK 32 +// Delay after detecting a vibe before shake/tap interrupts should be processed again +#define LIS2DW12_VIBE_COOLDOWN_MS (50) +#define LIS2DW12_REG_OPS_WAIT_TIME_MS (1) + +static int32_t prv_lis2dw12_write(void* handler, uint8_t reg, const uint8_t* data, uint16_t len); +static int32_t prv_lis2dw12_read(void* handler, uint8_t reg, uint8_t* data, uint16_t len); + +static stmdev_ctx_t lis2dw12_ctx = { + .read_reg = prv_lis2dw12_read, + .write_reg = prv_lis2dw12_write, +}; + +typedef struct { + lis2dw12_odr_t odr; + uint32_t interval_thr; + uint32_t interval; +} lis2dw12_sample_rate_t; + +static const lis2dw12_sample_rate_t s_lis2dw12_sample_rates[] = { + {LIS2DW12_XL_ODR_1Hz6_LP_ONLY, 200000, 625000}, + {LIS2DW12_XL_ODR_12Hz5, 60000, 80000}, + {LIS2DW12_XL_ODR_25Hz, 30000, 40000}, + {LIS2DW12_XL_ODR_50Hz, 15000, 20000}, + {LIS2DW12_XL_ODR_100Hz, 7500, 10000}, + {LIS2DW12_XL_ODR_200Hz, 3750, 5000}, + {LIS2DW12_XL_ODR_400Hz, 1875, 2500}, + {LIS2DW12_XL_ODR_800Hz, 937, 1250}, + {LIS2DW12_XL_ODR_1k6Hz, 468, 625}, +}; + +static bool s_lis2dw12_enabled = true; +static bool s_lis2dw12_running = false; +static bool s_fifo_in_use = false; // true when we have enabled FIFO batching +static uint32_t s_last_vibe_detected = 0; +// User-configured sensitivity percentage (0-100), where 100 = most sensitive +// Default to 100% (maximum sensitivity) to maintain current behavior +static uint8_t s_user_sensitivity_percent = 100; +// Error tracking and recovery +static uint32_t s_consecutive_errors = 0; +static bool s_sensor_health_ok = true; +static int16_t s_last_sample_mg[3] = {0}; +static uint64_t s_last_sample_timestamp_ms = 0; +// Interrupt activity instrumentation so we can spot when the sensor stops firing INT1. +static uint64_t s_last_interrupt_ms = 0; +static uint64_t s_last_wake_event_ms = 0; +static uint64_t s_last_double_tap_ms = 0; +static uint32_t s_interrupt_count = 0; +static uint32_t s_wake_event_count = 0; +static uint32_t s_double_tap_event_count = 0; + +typedef enum { + X_AXIS = 0, + Y_AXIS = 1, + Z_AXIS = 2, +} axis_t; + +typedef struct { + uint32_t sampling_interval_us; + uint32_t num_samples; + bool shake_detection_enabled; + bool shake_sensitivity_high; + bool double_tap_detection_enabled; +} lis2dw12_state_t; +lis2dw12_state_t s_lis2dw12_state = {0}; +lis2dw12_state_t s_lis2dw12_state_target = {0}; +static int32_t prv_lis2dw12_write(void* handler, uint8_t reg, const uint8_t* data, uint16_t len) { + i2c_use(I2C_LSM2DW12); + uint16_t data_len = len + sizeof(reg); + uint8_t data_w[data_len]; + data_w[0] = reg; + memcpy(data_w+1, data, len); + bool rv = i2c_write_block(I2C_LSM2DW12, data_len, data_w); + i2c_release(I2C_LSM2DW12); + + return !rv; +} + +static int32_t prv_lis2dw12_read(void* handler, uint8_t reg, uint8_t* data, uint16_t len) { + i2c_use(I2C_LSM2DW12); + bool rv = i2c_write_block(I2C_LSM2DW12, sizeof(reg), ®); + if (rv) { + rv = i2c_read_block(I2C_LSM2DW12, len, data); + } + i2c_release(I2C_LSM2DW12); + + return !rv; +} +static uint64_t prv_get_timestamp_ms(void) { + time_t time_s; + uint16_t time_ms; + rtc_get_time_ms(&time_s, &time_ms); + return (((uint64_t)time_s) * 1000 + time_ms); +} + +static bool prv_is_vibing(void) { + if (vibes_get_vibe_strength() != VIBE_STRENGTH_OFF) { + s_last_vibe_detected = prv_get_timestamp_ms(); + return true; + } + if (s_last_vibe_detected > 0) { + if (prv_get_timestamp_ms() - s_last_vibe_detected < LIS2DW12_VIBE_COOLDOWN_MS) { + return true; + } else { + s_last_vibe_detected = 0; // reset if cooldown expired + } + } + return false; +} + +static void prv_note_new_sample(const AccelDriverSample *sample) { + if (!sample) { + return; + } + + s_last_sample_mg[0] = sample->x; + s_last_sample_mg[1] = sample->y; + s_last_sample_mg[2] = sample->z; + + if (sample->timestamp_us != 0) { + s_last_sample_timestamp_ms = sample->timestamp_us / 1000ULL; + } else { + s_last_sample_timestamp_ms = prv_get_timestamp_ms(); + } +} + +static void prv_note_new_sample_mg(int16_t x_mg, int16_t y_mg, int16_t z_mg) { + AccelDriverSample sample = { + .x = x_mg, + .y = y_mg, + .z = z_mg, + .timestamp_us = prv_get_timestamp_ms() * 1000ULL, + }; + prv_note_new_sample(&sample); +} + +static int16_t prv_get_axis_projection_mg(axis_t axis, int16_t *raw_vector) { + uint8_t axis_offset = BOARD_CONFIG_ACCEL.accel_config.axes_offsets[axis]; + int axis_direction = BOARD_CONFIG_ACCEL.accel_config.axes_inverts[axis] ? -1 : 1; + + return lis2dw12_from_fs2_lp1_to_mg(raw_vector[axis_offset] * axis_direction); +} + +static uint8_t prv_lis2dw12_read_sample(AccelDriverSample *data) { + if (!s_lis2dw12_enabled) { + PBL_LOG(LOG_LEVEL_ERROR, "LIS2DW12: Not enabled, cannot read sample"); + return -1; + } + + int16_t accel_raw[3]; + if (lis2dw12_acceleration_raw_get(&lis2dw12_ctx, accel_raw) != 0) { + PBL_LOG(LOG_LEVEL_ERROR, "LIS2DW12: Failed to read accelerometer data"); + return -1; + } + + data->x = prv_get_axis_projection_mg(X_AXIS, accel_raw); + data->y = prv_get_axis_projection_mg(Y_AXIS, accel_raw); + data->z = prv_get_axis_projection_mg(Z_AXIS, accel_raw); + data->timestamp_us = prv_get_timestamp_ms() * 1000; + + prv_note_new_sample(data); + + if (s_lis2dw12_state.num_samples > 0) { + accel_cb_new_sample(data); + } + + return 0; +} + +// Accelerometer sample reading (and reporting) +static void prv_lis2dw12_read_samples(void) { + if (s_lis2dw12_state.num_samples <= 1 || !s_fifo_in_use) { + // Single sample path + AccelDriverSample sample; + prv_lis2dw12_read_sample(&sample); + return; + } + + // Drain FIFO + uint8_t fifo_level = 0; + if (lis2dw12_fifo_data_level_get(&lis2dw12_ctx, &fifo_level) != 0) { + PBL_LOG(LOG_LEVEL_ERROR, "LIS2DW12: Failed to read FIFO level"); + // Reset FIFO on communication error + lis2dw12_fifo_mode_set(&lis2dw12_ctx, LIS2DW12_BYPASS_MODE); + if (s_fifo_in_use) { + lis2dw12_fifo_mode_set(&lis2dw12_ctx, LIS2DW12_STREAM_MODE); + } + return; + } + if (fifo_level == 0) { + return; // nothing to do + } + + // Prevent infinite loops on stuck FIFO + if (fifo_level > LIS2DW12_FIFO_MAX_WATERMARK) { + PBL_LOG(LOG_LEVEL_ERROR, "LIS2DW12: FIFO level too high (%u), resetting", fifo_level); + // Reset FIFO on communication error + lis2dw12_fifo_mode_set(&lis2dw12_ctx, LIS2DW12_BYPASS_MODE); + if (s_fifo_in_use) { + lis2dw12_fifo_mode_set(&lis2dw12_ctx, LIS2DW12_STREAM_MODE); + } + return; + } + + const uint64_t now_us = prv_get_timestamp_ms() * 1000ULL; + const uint32_t interval_us = s_lis2dw12_state.sampling_interval_us ?: 1000; // avoid div by zero + + for (uint16_t i = 0; i < fifo_level; ++i) { + AccelDriverSample sample = {0}; + prv_lis2dw12_read_sample(&sample); + + // Approximate timestamp: assume fifo_level contiguous samples ending now + uint32_t sample_index_from_end = (fifo_level - 1) - i; // 0 for newest + sample.timestamp_us = now_us - (sample_index_from_end * (uint64_t)interval_us); + accel_cb_new_sample(&sample); + prv_note_new_sample(&sample); + } +} + +static void prv_lis2dw12_process_interrupts(void) { + const uint64_t now_ms = prv_get_timestamp_ms(); + const uint64_t previous_interrupt_ms = s_last_interrupt_ms; + s_last_interrupt_ms = now_ms; + s_interrupt_count++; + + uint32_t gap_ms = 0; + if (previous_interrupt_ms == 0) { + PBL_LOG(LOG_LEVEL_INFO, "LIS2DW12: First INT1 service (count=%lu)", + (unsigned long)s_interrupt_count); + } else { + uint64_t raw_gap_ms = now_ms - previous_interrupt_ms; + gap_ms = (raw_gap_ms > UINT32_MAX) ? UINT32_MAX : (uint32_t)raw_gap_ms; + if (gap_ms >= LIS2DW12_INTERRUPT_GAP_LOG_THRESHOLD_MS) { + PBL_LOG(LOG_LEVEL_INFO, + "LIS2DW12: INT1 gap %lu ms (count=%lu wake=%lu tap=%lu)", + (unsigned long)gap_ms, (unsigned long)s_interrupt_count, + (unsigned long)s_wake_event_count, (unsigned long)s_double_tap_event_count); + } + } + + // Read and clear interrupt sources atomically to prevent loss + lis2dw12_all_sources_t all_sources; + + // Multiple attempts to read interrupt sources in case of transient I2C issues + int read_attempts = 0; + const int max_read_attempts = 2; + + do { + if (lis2dw12_all_sources_get(&lis2dw12_ctx, &all_sources) == 0) { + break; // Success + } + read_attempts++; + if (read_attempts < max_read_attempts) { + // Brief delay and retry - this prevents losing interrupts due to transient I2C glitches + psleep(LIS2DW12_REG_OPS_WAIT_TIME_MS); + } + } while (read_attempts < max_read_attempts); + + if (read_attempts >= max_read_attempts) { + PBL_LOG(LOG_LEVEL_ERROR, "LIS2DW12: Failed to read interrupt sources after retries"); + s_consecutive_errors++; + if (s_consecutive_errors >= LIS2DW12_MAX_CONSECUTIVE_FAILURES) { + s_sensor_health_ok = false; + PBL_LOG(LOG_LEVEL_WARNING, "LIS2DW12: Interrupt processing failed, sensor health degraded"); + } + return; + } + + // Reset failure count on successful read + s_consecutive_errors = 0; + + if (all_sources.status_dup.ovr) { + PBL_LOG(LOG_LEVEL_WARNING, "LIS2DW12: FIFO overflow/full detected, clearing FIFO"); + + // Properly clear FIFO without losing configuration + uint8_t current_watermark; + + // Save current FIFO configuration + lis2dw12_fifo_watermark_get(&lis2dw12_ctx, ¤t_watermark); + + // Reset FIFO to bypass mode + lis2dw12_fifo_mode_set(&lis2dw12_ctx, LIS2DW12_BYPASS_MODE); + + // Wait for FIFO to actually clear + psleep(LIS2DW12_REG_OPS_WAIT_TIME_MS); + + // Clear all interrupt sources after FIFO reset to ensure clean state + lis2dw12_all_sources_t all_sources; + lis2dw12_all_sources_get(&lis2dw12_ctx, &all_sources); + + // Restore FIFO configuration if it was enabled + if (s_fifo_in_use) { + // Reduce watermark by half to prevent future overflow + uint8_t reduced_watermark = current_watermark / 2; + if (reduced_watermark == 0) reduced_watermark = 1; + + lis2dw12_fifo_watermark_get(&lis2dw12_ctx, &reduced_watermark); + lis2dw12_fifo_mode_set(&lis2dw12_ctx, LIS2DW12_STREAM_MODE); + + PBL_LOG(LOG_LEVEL_INFO, "LIS2DW12: Reduced FIFO watermark from %u to %u to prevent future overflow", + current_watermark, reduced_watermark); + } + + // Force re-enable of external interrupt to ensure it's active + exti_disable(BOARD_CONFIG_ACCEL.accel_ints[0]); + psleep(LIS2DW12_REG_OPS_WAIT_TIME_MS); + exti_enable(BOARD_CONFIG_ACCEL.accel_ints[0]); + } + + // Collect accelerometer samples if requested + if (s_lis2dw12_state.num_samples > 0 && (all_sources.status_dup.drdy || all_sources.status_dup.ovr)) { + prv_lis2dw12_read_samples(); + } + + // If currently vibing, any additional events should be ignored (they are + // likely spurious). + if (prv_is_vibing()) { + return; + } + + // Process double tap events + if (all_sources.tap_src.double_tap) { + s_double_tap_event_count++; + s_last_double_tap_ms = now_ms; + PBL_LOG(LOG_LEVEL_DEBUG, "LIS2DW12: Double tap interrupt triggered"); + // Handle double tap detection + axis_t axis; + if (all_sources.tap_src.x_tap) { + axis = X_AXIS; + } else if (all_sources.tap_src.y_tap) { + axis = Y_AXIS; + } else if (all_sources.tap_src.z_tap) { + axis = Z_AXIS; + } else { + PBL_LOG(LOG_LEVEL_DEBUG, "LIS2DW12: No tap axis detected"); + return; // No valid tap detected + } + + uint8_t axis_offset = BOARD_CONFIG_ACCEL.accel_config.axes_offsets[axis]; + uint8_t axis_direction = (BOARD_CONFIG_ACCEL.accel_config.axes_inverts[axis] ? -1 : 1) * + (all_sources.tap_src.tap_sign ? -1 : 1); + + PBL_LOG(LOG_LEVEL_DEBUG, "LIS2DW12: Double tap interrupt triggered; axis=%d, direction=%d", + axis_offset, axis_direction); + accel_cb_double_tap_detected(axis_offset, axis_direction); + } + + // Wake-up (any-motion) event -> treat as shake. Axis & direction derived from wake_up_src. + if (s_lis2dw12_state.shake_detection_enabled && all_sources.tap_src.tap_sign) { + s_wake_event_count++; + s_last_wake_event_ms = now_ms; + lis2dw12_wake_up_src_t wake_src; + if (lis2dw12_read_reg(&lis2dw12_ctx, LIS2DW12_WAKE_UP_SRC, (uint8_t *)&wake_src, 1) == 0) { + IMUCoordinateAxis axis = AXIS_X; + int32_t direction = 1; // LIS2DW12 does not give sign directly for wake-up; approximate via + // sign of latest sample on axis + // Determine which axis triggered: order X,Y,Z + const AccelConfig *cfg = &BOARD_CONFIG_ACCEL.accel_config; + if (wake_src.x_wu) { + axis = AXIS_X; + } else if (wake_src.y_wu) { + axis = AXIS_Y; + } else if (wake_src.z_wu) { + axis = AXIS_Z; + } + // Read current sample to infer direction + int16_t accel_raw[3]; + if (lis2dw12_acceleration_raw_get(&lis2dw12_ctx, accel_raw) == 0) { + int16_t val = accel_raw[cfg->axes_offsets[axis]]; + bool invert = cfg->axes_inverts[axis]; + direction = (val >= 0 ? 1 : -1) * (invert ? -1 : 1); + int16_t mg_x = prv_get_axis_projection_mg(X_AXIS, accel_raw); + int16_t mg_y = prv_get_axis_projection_mg(Y_AXIS, accel_raw); + int16_t mg_z = prv_get_axis_projection_mg(Z_AXIS, accel_raw); + prv_note_new_sample_mg(mg_x, mg_y, mg_z); + } + PBL_LOG(LOG_LEVEL_DEBUG, "LIS2DW12: Shake detected; axis=%d, direction=%lu", axis, direction); + accel_cb_shake_detected(axis, direction); + } + } +} + +static void prv_lis2dw12_interrupt_handler(bool *should_context_switch) { + // Offload processing to a worker. The LIS2DW12 can miss events if interrupts + // are ignored due to pending flags, so it is important to process them + // quickly. The actual clearing of the interrupt flags will happen in the + // worker via an I2C transaction. + accel_offload_work_from_isr(prv_lis2dw12_process_interrupts, should_context_switch); +} + +static void prv_lis2dw12_configure_fifo(bool enable) { + // Always (re)program watermark and batch rates when enabling or already enabled, + // but only flip FIFO mode when the enabled/disabled state changes. + if (enable) { + // Proper FIFO watermark calculation to prevent overflow + // Setting watermark too high can cause overflow and sensor lockup + + uint32_t watermark = s_lis2dw12_state.num_samples; + + // Set watermark to 50% of requested samples to prevent overflow + // This provides more buffer for timing variations and prevents lockup + watermark = watermark / 2; + if (watermark == 0) watermark = 1; // minimum + if (watermark > LIS2DW12_FIFO_MAX_WATERMARK) watermark = LIS2DW12_FIFO_MAX_WATERMARK; + + PBL_LOG(LOG_LEVEL_DEBUG, "LIS2DW12: Setting FIFO watermark to %lu (requested %lu samples)", + watermark, s_lis2dw12_state.num_samples); + + if (lis2dw12_fifo_watermark_set(&lis2dw12_ctx, (uint8_t)watermark)) { + PBL_LOG(LOG_LEVEL_ERROR, "LIS2DW12: Failed to set FIFO watermark"); + } + + // Always clear and re-enable FIFO to ensure clean state after configuration changes. + // This is critical when watermark changes while FIFO is already enabled, as stale + // samples in the FIFO can prevent new watermark interrupts from being generated. + // For example, if FIFO has 25 samples and watermark is lowered to 3, the sensor + // won't generate an interrupt because the FIFO already exceeds the watermark. + lis2dw12_fifo_mode_set(&lis2dw12_ctx, LIS2DW12_BYPASS_MODE); + psleep(LIS2DW12_REG_OPS_WAIT_TIME_MS); // Allow time for FIFO to clear + + // Put FIFO in stream mode so we keep collecting samples and get periodic watermark interrupts + if (lis2dw12_fifo_mode_set(&lis2dw12_ctx, LIS2DW12_STREAM_MODE)) { + PBL_LOG(LOG_LEVEL_ERROR, "LIS2DW12: Failed to enable FIFO stream mode"); + } + } else { + if (s_fifo_in_use) { + // Disable batching & return to bypass + if (lis2dw12_fifo_mode_set(&lis2dw12_ctx, LIS2DW12_BYPASS_MODE)) { + PBL_LOG(LOG_LEVEL_ERROR, "LIS2DW12: Failed to disable FIFO"); + } + } + } + + s_fifo_in_use = enable; + PBL_LOG(LOG_LEVEL_DEBUG, "LIS2DW12: FIFO %s (wm=%lu)", enable ? "enabled" : "disabled", + (unsigned long)s_lis2dw12_state.num_samples); +} + +void prv_lis2dw12_configure_double_tap(bool enable) { + if (enable) { + // Enable tap detection on all axes + lis2dw12_tap_detection_on_x_set(&lis2dw12_ctx, PROPERTY_ENABLE); + lis2dw12_tap_detection_on_y_set(&lis2dw12_ctx, PROPERTY_ENABLE); + lis2dw12_tap_detection_on_z_set(&lis2dw12_ctx, PROPERTY_ENABLE); + + // Configure tap timing + uint8_t tap_shock = BOARD_CONFIG_ACCEL.accel_config.tap_shock; + uint8_t tap_quiet = BOARD_CONFIG_ACCEL.accel_config.tap_quiet; + uint8_t tap_dur = BOARD_CONFIG_ACCEL.accel_config.tap_dur; + + lis2dw12_tap_shock_set(&lis2dw12_ctx, tap_shock); // Shock duration + lis2dw12_tap_quiet_set(&lis2dw12_ctx, tap_quiet); // Quiet period + lis2dw12_tap_dur_set(&lis2dw12_ctx, tap_dur); // Double tap window + + // Enable double tap recognition + lis2dw12_tap_mode_set(&lis2dw12_ctx, LIS2DW12_BOTH_SINGLE_DOUBLE); + } else { + // Disable tap detection + lis2dw12_tap_detection_on_x_set(&lis2dw12_ctx, PROPERTY_DISABLE); + lis2dw12_tap_detection_on_y_set(&lis2dw12_ctx, PROPERTY_DISABLE); + lis2dw12_tap_detection_on_z_set(&lis2dw12_ctx, PROPERTY_DISABLE); + } +} + +// Configure wake-up (any-motion) for shake detection using wake-up threshold & duration. +static void prv_lis2dw12_configure_shake(bool enable, bool sensitivity_high) { + if (!enable) { + // Disable wake-up related routing by clearing threshold + lis2dw12_wkup_threshold_set(&lis2dw12_ctx, 0); + return; + } + + // Duration: increase a bit to reduce spurious triggers + lis2dw12_wkup_dur_set(&lis2dw12_ctx, sensitivity_high ? 0 : 1); + + // Threshold calculation: + // - Board config provides Low and High thresholds + // - sensitivity_high flag indicates stationary mode (use low threshold for any movement) + // - s_user_sensitivity_percent (0-100) controls normal mode threshold + // * 100% = most sensitive = use Low threshold + // * 50% = medium = interpolate between Low and High + // * 0% = least sensitive = use High threshold + + uint32_t raw_high = BOARD_CONFIG_ACCEL.accel_config.shake_thresholds[AccelThresholdHigh]; + uint32_t raw_low = BOARD_CONFIG_ACCEL.accel_config.shake_thresholds[AccelThresholdLow]; + uint32_t raw; + + if (sensitivity_high) { + // Stationary mode: always use low threshold for maximum sensitivity + raw = raw_low; + } else { + // Normal mode: interpolate based on user preference + // Invert the percentage: 100% sensitive = low threshold, 0% sensitive = high threshold + uint32_t inverted_percent = 100 - s_user_sensitivity_percent; + raw = raw_low + ((raw_high - raw_low) * inverted_percent) / 100; + } + + // Clamp to valid range + if (raw > 63) raw = 63; // lis2dw12 wk_ths is 6 bits + if (raw < 2) raw = 2; // Avoid noise storms with very low thresholds + + lis2dw12_wkup_threshold_set(&lis2dw12_ctx, (uint8_t)raw); + + PBL_LOG(LOG_LEVEL_DEBUG, "LIS2DW12: Shake threshold set to %lu (sensitivity_high=%d, user_percent=%u)", + raw, sensitivity_high, s_user_sensitivity_percent); +} + +static uint32_t prv_lis2dw12_set_sampling_interval(uint32_t target_interval) { + for (uint8_t i=0 ; i= s_lis2dw12_sample_rates[i].interval_thr) { + lis2dw12_odr_t odr = s_lis2dw12_sample_rates[i].odr; + lis2dw12_data_rate_set(&lis2dw12_ctx, odr); + return s_lis2dw12_sample_rates[i].interval; + } + } + + PBL_LOG(LOG_LEVEL_ERROR, "LIS2DW12 can not get a suitable odr"); + return UINT32_MAX; +} + +static void prv_lis2dw12_configure_interrupts(void) { + // Disable interrupts during configuration to prevent race conditions + // and ensure atomic configuration updates + + bool should_enable_interrupts = s_lis2dw12_enabled && + (s_lis2dw12_state.num_samples || s_lis2dw12_state.shake_detection_enabled || + s_lis2dw12_state.double_tap_detection_enabled); + + // Always disable interrupts first to ensure clean state + exti_disable(BOARD_CONFIG_ACCEL.accel_ints[0]); + + if (!should_enable_interrupts) { + // Also disable all interrupt sources in the sensor to prevent phantom interrupts + lis2dw12_ctrl4_int1_pad_ctrl_t route = {0}; + if (lis2dw12_pin_int1_route_set(&lis2dw12_ctx, &route)) { + PBL_LOG(LOG_LEVEL_ERROR, "LIS2DW12: Failed to disable INT1 routes while turning off sensor"); + } + return; + } + + bool routing_configured = true; + + lis2dw12_ctrl4_int1_pad_ctrl_t int1_routes = {0}; + bool use_fifo = s_lis2dw12_state.num_samples > 1; // batching requested + + // Configure FIFO first, then set up interrupt routing + if (use_fifo) { + prv_lis2dw12_configure_fifo(true); + int1_routes.int1_diff5 = 1; + int1_routes.int1_fth = 1; + int1_routes.int1_drdy = 1; + } else { + prv_lis2dw12_configure_fifo(false); + int1_routes.int1_diff5 = 0; + int1_routes.int1_fth = 0; + int1_routes.int1_drdy = s_lis2dw12_state.num_samples > 0; // single-sample mode; + } + + int1_routes.int1_tap = s_lis2dw12_state.double_tap_detection_enabled; + int1_routes.int1_wu = s_lis2dw12_state.shake_detection_enabled; // use wake-up (any-motion) + + // Configure interrupt routing atomically + if (lis2dw12_pin_int1_route_set(&lis2dw12_ctx, &int1_routes) != 0) { + PBL_LOG(LOG_LEVEL_ERROR, "LIS2DW12: Failed to configure INT1 routes; re-enabling external interrupt"); + routing_configured = false; + } else { + // Clear any pending interrupt sources before enabling external interrupt + lis2dw12_all_sources_t all_sources; + if (lis2dw12_all_sources_get(&lis2dw12_ctx, &all_sources)) { + PBL_LOG(LOG_LEVEL_WARNING, "LIS2DW12: Failed to clear pending interrupt sources after routing update"); + } + } + + // Always re-enable the external interrupt so we do not lose future INT1 edges + exti_enable(BOARD_CONFIG_ACCEL.accel_ints[0]); + + if (!routing_configured) { + PBL_LOG(LOG_LEVEL_WARNING, "LIS2DW12: INT1 routing not updated; external interrupt left enabled for recovery"); + } +} + +// default odr off +void lis2dw12_init(void) { + uint8_t id; + int32_t ret = lis2dw12_device_id_get(&lis2dw12_ctx, &id); + if (ret || LIS2DW12_ID != id) { + PBL_LOG(LOG_LEVEL_ERROR, "Failed to get LIS2DW12 chip ID"); + return; + } + + /* Restore default configuration */ + ret = lis2dw12_reset_set(&lis2dw12_ctx, PROPERTY_ENABLE); + uint8_t rst; + int reset_timeout = 100; // 100ms max wait for reset + do { // Wait for reset to complete with timeout + psleep(LIS2DW12_REG_OPS_WAIT_TIME_MS); + if (lis2dw12_reset_get(&lis2dw12_ctx, &rst) != 0) { + PBL_LOG(LOG_LEVEL_ERROR, "LIS2DW12: Failed to read reset status"); + return; + } + reset_timeout--; + } while (rst && reset_timeout > 0); + + if (reset_timeout == 0) { + PBL_LOG(LOG_LEVEL_ERROR, "LIS2DW12: Reset timeout - sensor may be unresponsive"); + return; + } + + /* full scale: +/- 2g */ + if (lis2dw12_full_scale_set(&lis2dw12_ctx, LIS2DW12_2g)) { + PBL_LOG(LOG_LEVEL_ERROR, "Failed to set accelerometer scale"); + return; + } + + /* low power normal mode (no HP) */ + if (lis2dw12_power_mode_set(&lis2dw12_ctx, LIS2DW12_CONT_LOW_PWR_12bit)) { + PBL_LOG(LOG_LEVEL_ERROR, "Failed to set power mode"); + return; + } + + /* tap detection on all axis: X, Y, Z */ + if (lis2dw12_tap_detection_on_x_set(&lis2dw12_ctx, PROPERTY_ENABLE)) { + PBL_LOG(LOG_LEVEL_ERROR, "Failed to enable tap detection on X axis"); + return; + } + + if (lis2dw12_tap_detection_on_y_set(&lis2dw12_ctx, PROPERTY_ENABLE)) { + PBL_LOG(LOG_LEVEL_ERROR, "Failed to enable tap detection on Y axis"); + return; + } + + if (lis2dw12_tap_detection_on_z_set(&lis2dw12_ctx, PROPERTY_ENABLE)) { + PBL_LOG(LOG_LEVEL_ERROR, "Failed to enable tap detection on Z axis"); + return; + } + + /* X,Y,Z threshold: 1 * FS_XL / 2^5 = 1 * 2 / 32 = 62.5 mg */ + if (lis2dw12_tap_threshold_x_set(&lis2dw12_ctx, 1)) { + PBL_LOG(LOG_LEVEL_ERROR, "Failed to set tap threshold on X axis"); + return; + } + + if (lis2dw12_tap_threshold_y_set(&lis2dw12_ctx, 1)) { + PBL_LOG(LOG_LEVEL_ERROR, "Failed to set tap threshold on Y axis"); + return; + } + + if (lis2dw12_tap_threshold_z_set(&lis2dw12_ctx, 1)) { + PBL_LOG(LOG_LEVEL_ERROR, "Failed to set tap threshold on Z axis"); + return; + } + + /* shock time: 2 / ODR_XL = 2 / 26 ~= 77 ms */ + if (lis2dw12_tap_shock_set(&lis2dw12_ctx, 0)) { + PBL_LOG(LOG_LEVEL_ERROR, "Failed to set tap shock duration"); + return; + } + + /* quiet time: 2 / ODR_XL = 2 / 26 ~= 77 ms */ + if (lis2dw12_tap_quiet_set(&lis2dw12_ctx, 0)) { + PBL_LOG(LOG_LEVEL_ERROR, "Failed to set tap quiet duration"); + return; + } + + /* route single rap to INT1 */ + lis2dw12_ctrl4_int1_pad_ctrl_t route = {.int1_single_tap = 1}; + if (lis2dw12_pin_int1_route_set(&lis2dw12_ctx, &route)) { + PBL_LOG(LOG_LEVEL_ERROR, "Failed to route interrupt"); + return; + } + + /* data rate: default off */ + if (lis2dw12_data_rate_set(&lis2dw12_ctx, LIS2DW12_XL_ODR_OFF)) { + PBL_LOG(LOG_LEVEL_ERROR, "Failed to set accelerometer data rate"); + return; + } + + exti_configure_pin(BOARD_CONFIG_ACCEL.accel_ints[0], ExtiTrigger_Rising, + prv_lis2dw12_interrupt_handler); + + if (lis2dw12_data_ready_mode_set(&lis2dw12_ctx, LIS2DW12_DRDY_PULSED)) { + PBL_LOG(LOG_LEVEL_ERROR, "Failed to set accelerometer data ready mode"); + return; + } + if (lis2dw12_int_notification_set(&lis2dw12_ctx, LIS2DW12_INT_PULSED)) { + PBL_LOG(LOG_LEVEL_ERROR, "Failed to set accelerometer int notification mode"); + return; + } +} + +//! Synchronize the LIS2DW12 state with the desired target state. +static void prv_lis2dw12_chase_target_state(void) { + bool update_interrupts = false; + + // Check whether we should be spinning up the accelerometer + bool should_be_running = s_lis2dw12_state_target.sampling_interval_us > 0 || + s_lis2dw12_state_target.num_samples > 0 || + s_lis2dw12_state_target.shake_detection_enabled || + s_lis2dw12_state_target.double_tap_detection_enabled; + + if (!should_be_running || !s_lis2dw12_enabled) { + if (s_lis2dw12_running) { + PBL_LOG(LOG_LEVEL_DEBUG, "LIS2DW12: Stopping accelerometer"); + lis2dw12_data_rate_set(&lis2dw12_ctx, LIS2DW12_XL_ODR_OFF); + s_lis2dw12_running = false; + s_lis2dw12_state = (lis2dw12_state_t){0}; + prv_lis2dw12_configure_interrupts(); + } + return; + } else if (!s_lis2dw12_running) { + s_lis2dw12_running = true; + update_interrupts = true; + } + + // Update number of samples + if (s_lis2dw12_state_target.num_samples != s_lis2dw12_state.num_samples) { + s_lis2dw12_state.num_samples = s_lis2dw12_state_target.num_samples; + update_interrupts = true; + } + + // Update shake detection + if (s_lis2dw12_state_target.shake_detection_enabled != s_lis2dw12_state.shake_detection_enabled || + s_lis2dw12_state_target.shake_sensitivity_high != s_lis2dw12_state.shake_sensitivity_high) { + s_lis2dw12_state.shake_detection_enabled = s_lis2dw12_state_target.shake_detection_enabled; + s_lis2dw12_state.shake_sensitivity_high = s_lis2dw12_state_target.shake_sensitivity_high; + prv_lis2dw12_configure_shake(s_lis2dw12_state.shake_detection_enabled, + s_lis2dw12_state.shake_sensitivity_high); + update_interrupts = true; + } + + // Update double tap detection + if (s_lis2dw12_state_target.double_tap_detection_enabled != + s_lis2dw12_state.double_tap_detection_enabled) { + prv_lis2dw12_configure_double_tap(s_lis2dw12_state_target.double_tap_detection_enabled); + s_lis2dw12_state.double_tap_detection_enabled = + s_lis2dw12_state_target.double_tap_detection_enabled; + update_interrupts = true; + } + + // Update sampling interval. Ensure ODR is enabled when event-only features are active. + if (update_interrupts || + s_lis2dw12_state_target.sampling_interval_us != s_lis2dw12_state.sampling_interval_us) { + uint32_t requested_interval = s_lis2dw12_state_target.sampling_interval_us; + + //default ODR to 100Hz + if(requested_interval == 0) requested_interval = 10 * 1000; + s_lis2dw12_state.sampling_interval_us = prv_lis2dw12_set_sampling_interval(requested_interval); + } + + // Update interrupts if necessary + if (update_interrupts) { + prv_lis2dw12_configure_interrupts(); + } + + // Note: Do NOT reset target state here as it creates a race condition + // where new target changes during this function execution could be lost. + // Instead, only sync the fields that were actually processed. + + PBL_LOG(LOG_LEVEL_DEBUG, + "LIS2DW12: Reached target state: sampling_interval_us=%lu, num_samples=%lu, " + "shake_detection_enabled=%d, shake_high_sensitivity=%d, double_tap_detection_enabled=%d", + s_lis2dw12_state.sampling_interval_us, s_lis2dw12_state.num_samples, + s_lis2dw12_state.shake_detection_enabled, s_lis2dw12_state.shake_sensitivity_high, + s_lis2dw12_state.double_tap_detection_enabled); +} + +void lis2dw12_power_up(void) { + s_lis2dw12_enabled = true; + prv_lis2dw12_chase_target_state(); +} + +void lis2dw12_power_down(void) { + PBL_LOG(LOG_LEVEL_DEBUG, "LIS2DW12: Powering down accelerometer"); + s_lis2dw12_enabled = false; + prv_lis2dw12_chase_target_state(); +} + +uint32_t accel_set_sampling_interval(uint32_t interval_us) { + PBL_LOG(LOG_LEVEL_DEBUG, "LIS2DW12: Requesting update of sampling interval to %lu us", + interval_us); + s_lis2dw12_state_target.sampling_interval_us = interval_us; + prv_lis2dw12_chase_target_state(); + return s_lis2dw12_state.sampling_interval_us; +} + +uint32_t accel_get_sampling_interval(void) { return s_lis2dw12_state.sampling_interval_us; } + +void accel_set_num_samples(uint32_t num_samples) { + PBL_LOG(LOG_LEVEL_DEBUG, "LIS2DW12: Setting number of samples to %lu", num_samples); + s_lis2dw12_state_target.num_samples = num_samples; + prv_lis2dw12_chase_target_state(); +} + +int accel_peek(AccelDriverSample *data) { return prv_lis2dw12_read_sample(data); } + +void accel_enable_shake_detection(bool on) { + PBL_LOG(LOG_LEVEL_DEBUG, "LIS2DW12: %s shake detection.", on ? "Enabling" : "Disabling"); + s_lis2dw12_state_target.shake_detection_enabled = on; + prv_lis2dw12_chase_target_state(); + +} + +bool accel_get_shake_detection_enabled(void) { return s_lis2dw12_state.shake_detection_enabled; } + +void accel_enable_double_tap_detection(bool on) { + PBL_LOG(LOG_LEVEL_DEBUG, "LIS2DW12: %s double tap detection.", on ? "Enabling" : "Disabling"); + s_lis2dw12_state_target.double_tap_detection_enabled = on; + prv_lis2dw12_chase_target_state(); +} + +bool accel_get_double_tap_detection_enabled(void) { + return s_lis2dw12_state.double_tap_detection_enabled; +} + +void accel_set_shake_sensitivity_high(bool sensitivity_high) { + PBL_LOG(LOG_LEVEL_DEBUG, "LIS2DW12: Setting shake sensitivity to %s.", + sensitivity_high ? "high" : "normal"); + s_lis2dw12_state_target.shake_sensitivity_high = sensitivity_high; + prv_lis2dw12_chase_target_state(); +} + +void accel_set_shake_sensitivity_percent(uint8_t percent) { + if (percent > 100) { + percent = 100; // Clamp to max + } + + s_user_sensitivity_percent = percent; + + // Reconfigure shake detection if it's currently enabled + if (s_lis2dw12_state.shake_detection_enabled) { + prv_lis2dw12_configure_shake(true, s_lis2dw12_state.shake_sensitivity_high); + } + + PBL_LOG(LOG_LEVEL_INFO, "LIS2DW12: User sensitivity set to %u percent", percent); +} + +bool accel_run_selftest(void) { + //TODO: implement selftest function + return true; +} diff --git a/src/fw/drivers/imu/lis2dw12/lis2dw12.h b/src/fw/drivers/imu/lis2dw12/lis2dw12.h new file mode 100644 index 000000000..22220662b --- /dev/null +++ b/src/fw/drivers/imu/lis2dw12/lis2dw12.h @@ -0,0 +1,21 @@ +/* + * Copyright 2025 Core Devices LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +extern void lis2dw12_init(void); +extern void lis2dw12_power_up(void); +extern void lis2dw12_power_down(void); \ No newline at end of file diff --git a/src/fw/drivers/imu/lsm6dso/lsm6dso.c b/src/fw/drivers/imu/lsm6dso/lsm6dso.c index d091b50a9..fd639467f 100644 --- a/src/fw/drivers/imu/lsm6dso/lsm6dso.c +++ b/src/fw/drivers/imu/lsm6dso/lsm6dso.c @@ -118,6 +118,9 @@ static RegularTimerInfo s_interrupt_watchdog_timer = { .cb_data = NULL }; +// watch rotation +static bool s_rotated_180 = false; + // Maximum FIFO watermark supported by hardware (diff_fifo is 10 bits -> 0..1023) #define LSM6DSO_FIFO_MAX_WATERMARK 1023 @@ -205,6 +208,21 @@ void accel_set_shake_sensitivity_high(bool sensitivity_high) { prv_lsm6dso_chase_target_state(); } +void accel_set_shake_sensitivity_percent(uint8_t percent) { + if (percent > 100) { + percent = 100; // Clamp to max + } + + s_user_sensitivity_percent = percent; + + // Reconfigure shake detection if it's currently enabled + if (s_lsm6dso_state.shake_detection_enabled) { + prv_lsm6dso_configure_shake(true, s_lsm6dso_state.shake_sensitivity_high); + } + + PBL_LOG(LOG_LEVEL_INFO, "LSM6DSO: User sensitivity set to %u percent", percent); +} + // HAL context implementations static int32_t prv_lsm6dso_read(void *handle, uint8_t reg_addr, uint8_t *buffer, @@ -1128,8 +1146,10 @@ static uint8_t prv_lsm6dso_read_sample(AccelDriverSample *data) { return -1; } - data->x = prv_get_axis_projection_mg(X_AXIS, accel_raw); - data->y = prv_get_axis_projection_mg(Y_AXIS, accel_raw); + data->x = s_rotated_180 ? prv_get_axis_projection_mg(X_AXIS, accel_raw) * -1 + : prv_get_axis_projection_mg(X_AXIS, accel_raw); + data->y = s_rotated_180 ? prv_get_axis_projection_mg(Y_AXIS, accel_raw) * -1 + : prv_get_axis_projection_mg(Y_AXIS, accel_raw); data->z = prv_get_axis_projection_mg(Z_AXIS, accel_raw); data->timestamp_us = prv_get_timestamp_ms() * 1000; @@ -1386,17 +1406,6 @@ void lsm6dso_get_diagnostics(Lsm6dsoDiagnostics *diagnostics) { diagnostics->state_flags = flags; } -void lsm6dso_set_sensitivity_percent(uint8_t percent) { - if (percent > 100) { - percent = 100; // Clamp to max - } - - s_user_sensitivity_percent = percent; - - // Reconfigure shake detection if it's currently enabled - if (s_lsm6dso_state.shake_detection_enabled) { - prv_lsm6dso_configure_shake(true, s_lsm6dso_state.shake_sensitivity_high); - } - - PBL_LOG(LOG_LEVEL_INFO, "LSM6DSO: User sensitivity set to %u percent", percent); -} +void imu_set_rotated(bool rotated) { + s_rotated_180 = rotated; +} \ No newline at end of file diff --git a/src/fw/drivers/imu/lsm6dso/lsm6dso.h b/src/fw/drivers/imu/lsm6dso/lsm6dso.h index 90d856fcc..7c920cdf5 100644 --- a/src/fw/drivers/imu/lsm6dso/lsm6dso.h +++ b/src/fw/drivers/imu/lsm6dso/lsm6dso.h @@ -56,3 +56,5 @@ void lsm6dso_power_down(void); //! Retrieve a snapshot of sensor diagnostics for telemetry. void lsm6dso_get_diagnostics(Lsm6dsoDiagnostics *diagnostics); + +void imu_set_rotated(bool rotated); diff --git a/src/fw/drivers/imu/mmc5603nj/mmc5603nj.c b/src/fw/drivers/imu/mmc5603nj/mmc5603nj.c index 5bb173acb..2b677a27b 100644 --- a/src/fw/drivers/imu/mmc5603nj/mmc5603nj.c +++ b/src/fw/drivers/imu/mmc5603nj/mmc5603nj.c @@ -56,6 +56,9 @@ static TimerID s_polling_timer = TIMER_INVALID_ID; static uint16_t s_polling_interval_ms = 0; static bool s_measurement_ready = false; +// watch rotation +static bool s_rotated_180 = false; + // MMC5603NJ entrypoints void mmc5603nj_init(void) { @@ -341,13 +344,19 @@ static MagReadStatus prv_mmc5603nj_get_sample(MagData *sample) { (int16_t)(raw_axis_value - (1 << 15)); // offset by 2^15 for uint -> int alignment } - sample->x = prv_get_axis_projection(X_AXIS, raw_vector); - sample->y = prv_get_axis_projection(Y_AXIS, raw_vector); + sample->x = s_rotated_180 ? prv_get_axis_projection(X_AXIS, raw_vector) * -1 + : prv_get_axis_projection(X_AXIS, raw_vector); + sample->y = s_rotated_180 ? prv_get_axis_projection(Y_AXIS, raw_vector) * -1 + : prv_get_axis_projection(Y_AXIS, raw_vector); sample->z = prv_get_axis_projection(Z_AXIS, raw_vector); return MagReadSuccess; } +void mag_set_rotated(bool rotated) { + s_rotated_180 = rotated; +} + static int16_t prv_get_axis_projection(axis_t axis, int16_t *raw_vector) { uint8_t axis_offset = BOARD_CONFIG_MAG.mag_config.axes_offsets[axis]; bool invert = BOARD_CONFIG_MAG.mag_config.axes_inverts[axis]; diff --git a/src/fw/drivers/imu/mmc5603nj/mmc5603nj.h b/src/fw/drivers/imu/mmc5603nj/mmc5603nj.h index f28379ca9..504f0e5a6 100644 --- a/src/fw/drivers/imu/mmc5603nj/mmc5603nj.h +++ b/src/fw/drivers/imu/mmc5603nj/mmc5603nj.h @@ -19,3 +19,5 @@ #pragma once void mmc5603nj_init(void); + +void mag_set_rotated(bool rotated); diff --git a/src/fw/drivers/nrf5/button.c b/src/fw/drivers/nrf5/button.c index e1cec3032..5ecb3dd35 100644 --- a/src/fw/drivers/nrf5/button.c +++ b/src/fw/drivers/nrf5/button.c @@ -7,8 +7,21 @@ #include "kernel/events.h" #include "system/passert.h" +// watch rotation +static bool s_rotated_180 = false; + +void button_set_rotated(bool rotated) { + s_rotated_180 = rotated; +} + bool button_is_pressed(ButtonId id) { - const ButtonConfig* button_config = &BOARD_CONFIG_BUTTON.buttons[id]; + if (s_rotated_180 && id == BUTTON_ID_UP) { + id = BUTTON_ID_DOWN; + } else if (s_rotated_180 && id == BUTTON_ID_DOWN) { + id = BUTTON_ID_UP; + } + + const ButtonConfig *button_config = &BOARD_CONFIG_BUTTON.buttons[id]; uint32_t bit = nrf_gpio_pin_read(button_config->gpiote.gpio_pin); return (BOARD_CONFIG_BUTTON.active_high) ? bit : !bit; diff --git a/src/fw/drivers/sf32lb52/lptim_systick.c b/src/fw/drivers/sf32lb52/lptim_systick.c index 75649cc76..aa0bd79d5 100644 --- a/src/fw/drivers/sf32lb52/lptim_systick.c +++ b/src/fw/drivers/sf32lb52/lptim_systick.c @@ -37,6 +37,8 @@ #error "lptim systick not compatible with LXT" #endif +#define LPTIM_COUNT_MAX 0xFFFFU + static LPTIM_HandleTypeDef s_lptim1_handle = {0}; static bool s_lptim_systick_initialized = false; static uint32_t s_last_idle_counter = 0; @@ -70,7 +72,7 @@ void lptim_systick_enable(void) { __HAL_LPTIM_ENABLE(&s_lptim1_handle); __HAL_LPTIM_COUNTRST_RESET(&s_lptim1_handle); - __HAL_LPTIM_AUTORELOAD_SET(&s_lptim1_handle, 0xFFFF); + __HAL_LPTIM_AUTORELOAD_SET(&s_lptim1_handle, LPTIM_COUNT_MAX); __HAL_LPTIM_COMPARE_SET(&s_lptim1_handle, SYSTICK_ONE_TICK_HZ); __HAL_LPTIM_ENABLE_IT(&s_lptim1_handle, LPTIM_IT_OCIE); @@ -86,8 +88,8 @@ void lptim_systick_tickless_idle(uint32_t ticks_from_now) s_last_idle_counter = counter; counter += ticks_from_now * SYSTICK_ONE_TICK_HZ; - if (counter >= 0xFFFF) { - counter -= 0xFFFF; + if (counter >= LPTIM_COUNT_MAX) { + counter -= LPTIM_COUNT_MAX; } __HAL_LPTIM_COMPARE_SET(&s_lptim1_handle, counter); @@ -99,7 +101,7 @@ uint32_t lptim_systick_get_elapsed_ticks(void) uint32_t counter = LPTIM1->CNT; if (counter < s_last_idle_counter) { - counter += 0x10000; + counter += (LPTIM_COUNT_MAX + 1); } return (counter - s_last_idle_counter) / SYSTICK_ONE_TICK_HZ; @@ -110,8 +112,8 @@ static inline void lptim_systick_next_tick_setup(void) uint32_t counter = LPTIM1->CNT; counter += SYSTICK_ONE_TICK_HZ; - if (counter >= 0xFFFF) { - counter -= 0xFFFF; + if (counter >= LPTIM_COUNT_MAX) { + counter -= LPTIM_COUNT_MAX; } __HAL_LPTIM_COMPARE_SET(&s_lptim1_handle, counter); @@ -119,7 +121,8 @@ static inline void lptim_systick_next_tick_setup(void) void LPTIM1_IRQHandler(void) { - static uint32_t wdt_feed_cnt = 0U; + static uint32_t wdt_last_counter = 0U; + static uint32_t wdt_feed_counter = 0U; if (__HAL_LPTIM_GET_FLAG(&s_lptim1_handle, LPTIM_FLAG_OC) != RESET) { __HAL_LPTIM_CLEAR_FLAG(&s_lptim1_handle, LPTIM_IT_OCIE); @@ -129,18 +132,32 @@ void LPTIM1_IRQHandler(void) if (__HAL_LPTIM_GET_FLAG(&s_lptim1_handle, LPTIM_FLAG_OCWKUP) == RESET) { extern void SysTick_Handler(); SysTick_Handler(); - } - wdt_feed_cnt++; - if (wdt_feed_cnt >= (RTC_TICKS_HZ * TASK_WATCHDOG_FEED_PERIOD_MS) / 1000) { - wdt_feed_cnt = 0U; - task_watchdog_feed(); + uint32_t current_counter = LPTIM1->CNT; + if (current_counter < wdt_last_counter) { + current_counter += (LPTIM_COUNT_MAX + 1); + } + wdt_feed_counter += (current_counter - wdt_last_counter); + wdt_last_counter = current_counter & LPTIM_COUNT_MAX; + if (wdt_feed_counter >= (TASK_WATCHDOG_FEED_PERIOD_MS * SYSTICK_ONE_TICK_HZ)) { + wdt_feed_counter = 0U; + task_watchdog_feed(); + } } } if (__HAL_LPTIM_GET_FLAG(&s_lptim1_handle, LPTIM_FLAG_OCWKUP) != RESET) { __HAL_LPTIM_DISABLE_IT(&s_lptim1_handle, LPTIM_IT_OCWE); __HAL_LPTIM_CLEAR_FLAG(&s_lptim1_handle, LPTIM_ICR_WKUPCLR); + + // Force a watchdog refresh immediately after wakeup. The LPTIM SysTick requires + // time to restart; if the system re-enters Stop mode during this latency, a watchdog + // timeout may occur. + task_watchdog_bit_set_all(); + task_watchdog_feed(); + // refresh wdt feed counter + wdt_feed_counter = 0; + wdt_last_counter = LPTIM1->CNT; } } diff --git a/src/fw/drivers/stubs/button.c b/src/fw/drivers/stubs/button.c index 2f8f05644..d1c678b3d 100644 --- a/src/fw/drivers/stubs/button.c +++ b/src/fw/drivers/stubs/button.c @@ -14,6 +14,10 @@ uint8_t button_get_state_bits(void) { void button_init(void) { } +void button_set_rotated(bool rotated) { + +} + bool button_selftest(void) { return true; } diff --git a/src/fw/drivers/stubs/display.c b/src/fw/drivers/stubs/display.c index 2c2042bb1..12af376e8 100644 --- a/src/fw/drivers/stubs/display.c +++ b/src/fw/drivers/stubs/display.c @@ -13,6 +13,10 @@ void display_clear(void) { void display_set_enabled(bool enabled) { } +void display_set_rotated(bool rotated) { + +} + bool display_update_in_progress(void) { return true; } diff --git a/src/fw/drivers/wscript_build b/src/fw/drivers/wscript_build index f72e6153b..375cf351d 100644 --- a/src/fw/drivers/wscript_build +++ b/src/fw/drivers/wscript_build @@ -486,7 +486,7 @@ if bld.is_silk() and not bld.env.QEMU: ], ) -if bld.is_asterix() or bld.is_obelix(): +if bld.is_asterix() or bld.is_obelix_evt() or bld.is_obelix_bb(): bld.objects( name='driver_lsm6dso', source=[ @@ -498,6 +498,19 @@ if bld.is_asterix() or bld.is_obelix(): ], ) +# NOTE: enable LIS2DW12 by default on Obelix DVT/PVT/BB2 +if bld.is_obelix_dvt() or bld.is_obelix_pvt() or bld.is_obelix_bb2(): + bld.objects( + name='driver_lis2dw12', + source=[ + 'imu/lis2dw12/lis2dw12.c', + ], + use=[ + 'hal_lis2dw12', + 'fw_includes', + ], + ) + if mcu_family in ('STM32F2', 'STM32F4', 'STM32F7'): bld.objects( name='driver_button', @@ -1055,17 +1068,25 @@ elif bld.is_asterix(): ], ) elif bld.is_obelix(): + use = [ + 'driver_mmc5603nj', + 'fw_includes', + 'root_includes', + ] + + # NOTE: enable LIS2DW12 by default on Obelix DVT/PVT/BB2 + if bld.is_obelix_dvt() or bld.is_obelix_pvt() or bld.is_obelix_bb2(): + use += ['driver_lis2dw12'] + bld.env.DEFINES.append('IMU_USE_LIS2DW12') + else: + use += ['driver_lsm6dso'] + bld.objects( name='driver_imu', source=[ 'imu/imu_obelix.c', ], - use=[ - 'driver_lsm6dso', - 'driver_mmc5603nj', - 'fw_includes', - 'root_includes', - ], + use=use, ) if bld.env.BOARD == "v2_0": @@ -1085,7 +1106,7 @@ if bld.env.BOARD == "v2_0": ], ) elif bld.is_obelix(): - if not bld.is_obelix_dvt() and not bld.is_obelix_bb2(): + if not bld.is_obelix_dvt() and not bld.is_obelix_pvt() and not bld.is_obelix_bb2(): bld.objects( name='driver_led_controller', source=[ diff --git a/src/fw/flash_region/flash_region_gd25q256e.h b/src/fw/flash_region/flash_region_gd25q256e.h index 6dcee8b4c..281623514 100644 --- a/src/fw/flash_region/flash_region_gd25q256e.h +++ b/src/fw/flash_region/flash_region_gd25q256e.h @@ -36,8 +36,8 @@ MACRO(FIRMWARE_SLOT_1, 0x0300000 /* 3072K */, arg) /* 0x12320000 - 0x1261FFFF */ \ MACRO(SYSTEM_RESOURCES_BANK_0, 0x0200000 /* 2048K */, arg) /* 0x12620000 - 0x1281FFFF */ \ MACRO(SYSTEM_RESOURCES_BANK_1, 0x0200000 /* 2048K */, arg) /* 0x12820000 - 0x12A1FFFF */ \ - MACRO(SAFE_FIRMWARE, 0x0080000 /* 512K */, arg) /* 0x12A20000 - 0x12A9FFFF */ \ - MACRO(FILESYSTEM, 0x1520000 /* 21632K */, arg) /* 0x12AA0000 - 0x13FBFFFF */ \ + MACRO(SAFE_FIRMWARE, 0x0090000 /* 576K */, arg) /* 0x12A20000 - 0x12AAFFFF */ \ + MACRO(FILESYSTEM, 0x1510000 /* 21568K */, arg) /* 0x12AB0000 - 0x13FBFFFF */ \ MACRO(RSVD1, 0x000F000 /* 60K */, arg) /* 0x13FC0000 - 0x13FCEFFF */ \ MACRO(DEBUG_DB, 0x0020000 /* 128K */, arg) /* 0x13FCF000 - 0x13FEEFFF */ \ MACRO(RSVD2, 0x000E000 /* 54K */, arg) /* 0x13FEF000 - 0x13FFCFFF */ \ diff --git a/src/fw/services/common/accel_manager.c b/src/fw/services/common/accel_manager.c index 88d5d03b9..a11094c95 100644 --- a/src/fw/services/common/accel_manager.c +++ b/src/fw/services/common/accel_manager.c @@ -405,32 +405,20 @@ void analytics_external_collect_accel_samples_received(void) { void accel_manager_update_sensitivity(uint8_t sensitivity_percent) { // Sensitivity mapping: // - sensitivity_percent: 0-100 where higher = more sensitive - // - For Asterix with LSM6DSO, this maps to the wake-up threshold + // - For those that support it, this maps to the wake-up threshold // - Lower threshold = more sensitive (triggers on smaller movements) // - Higher threshold = less sensitive (requires larger movements to trigger) // // We'll map the user's percentage to a threshold multiplier: - // - 100% (most sensitive) = use Low threshold (15) - // - 50% (medium) = use mid-range (~40) - // - 0% (least sensitive) = use High threshold (64) + // - 100% (most sensitive) = use Low threshold + // - 50% (medium) = use mid-range + // - 0% (least sensitive) = use High threshold - #if PLATFORM_ASTERIX - extern void lsm6dso_set_sensitivity_percent(uint8_t percent); mutex_lock_recursive(s_accel_manager_mutex); - lsm6dso_set_sensitivity_percent(sensitivity_percent); + accel_set_shake_sensitivity_percent(sensitivity_percent); mutex_unlock_recursive(s_accel_manager_mutex); PBL_LOG(LOG_LEVEL_INFO, "Motion sensitivity updated to %u percent", sensitivity_percent); - #else - // For other platforms, fall back to binary high/low setting - bool use_high_sensitivity = (sensitivity_percent >= 50); - mutex_lock_recursive(s_accel_manager_mutex); - accel_set_shake_sensitivity_high(use_high_sensitivity); - mutex_unlock_recursive(s_accel_manager_mutex); - - PBL_LOG(LOG_LEVEL_INFO, "Motion sensitivity updated to %u percent (using %s sensitivity)", - sensitivity_percent, use_high_sensitivity ? "high" : "normal"); - #endif } void accel_manager_init(void) { @@ -453,7 +441,7 @@ void accel_manager_init(void) { // Apply saved motion sensitivity preference for Asterix/Obelix // Only available in normal shell (not PRF) - #if (PLATFORM_ASTERIX) && !defined(RECOVERY_FW) + #if (PLATFORM_ASTERIX || PLATFORM_OBELIX) && !defined(RECOVERY_FW) extern uint8_t shell_prefs_get_motion_sensitivity(void); uint8_t saved_sensitivity = shell_prefs_get_motion_sensitivity(); accel_manager_update_sensitivity(saved_sensitivity); diff --git a/src/fw/services/common/light.c b/src/fw/services/common/light.c index ec8feaf1e..31544de8b 100644 --- a/src/fw/services/common/light.c +++ b/src/fw/services/common/light.c @@ -124,9 +124,9 @@ static uint16_t prv_backlight_get_intensity(void) { // Low intensity is always 5% (the "Low" setting) const uint16_t low_intensity = (BACKLIGHT_BRIGHTNESS_MAX * (uint32_t)5) / 100; - // Get thresholds from board config - const uint32_t min_light_threshold = BOARD_CONFIG.dynamic_backlight_min_threshold; - const uint32_t max_light_threshold = BOARD_CONFIG.dynamic_backlight_max_threshold; + // Get thresholds from preferences (allows runtime adjustment in debug menu) + const uint32_t min_light_threshold = backlight_get_dynamic_min_threshold(); + const uint32_t max_light_threshold = backlight_get_dynamic_max_threshold(); // If below minimum threshold, return low intensity if (light_level < min_light_threshold) { @@ -433,6 +433,11 @@ DEFINE_SYSCALL(void, sys_light_reset_to_timed_mode, void) { extern BacklightBehaviour backlight_get_behaviour(void); +uint8_t light_get_current_brightness_percent(void) { + uint8_t percent = (s_current_brightness * 100) / BACKLIGHT_BRIGHTNESS_MAX; + return percent; +} + void analytics_external_collect_backlight_settings(void) { BacklightBehaviour behaviour = backlight_get_behaviour(); bool is_motion_enabled = backlight_is_motion_enabled(); diff --git a/src/fw/services/common/light.h b/src/fw/services/common/light.h index cd47a5b20..1afa050a6 100644 --- a/src/fw/services/common/light.h +++ b/src/fw/services/common/light.h @@ -68,5 +68,10 @@ void light_toggle_ambient_sensor_enabled(void); //! Switches for temporary disabling backlight (ie: low power mode) void light_allow(bool allowed); +//! Get the current active backlight brightness as a percentage (0-100) +//! This returns the actual current brightness, which may differ from the +//! configured brightness when dynamic backlight is enabled. +uint8_t light_get_current_brightness_percent(void); + //! @} // group Light //! @} // group UI diff --git a/src/fw/services/normal/orientation_manager.c b/src/fw/services/normal/orientation_manager.c new file mode 100644 index 000000000..8ae6f4ee5 --- /dev/null +++ b/src/fw/services/normal/orientation_manager.c @@ -0,0 +1,24 @@ +#if PLATFORM_ASTERIX +#include "services/normal/orientation_manager.h" +#include "system/passert.h" +#include "shell/prefs.h" +#include "drivers/display/display.h" +#include "drivers/button.h" +#include "drivers/imu/lsm6dso/lsm6dso.h" +#include "drivers/imu/mmc5603nj/mmc5603nj.h" + +void prv_change_orientation(bool rotated) { + display_set_rotated(rotated); + button_set_rotated(rotated); + imu_set_rotated(rotated); + mag_set_rotated(rotated); +} + +void orientation_handle_prefs_changed(void) { + prv_change_orientation(display_orientation_is_left()); +} + +void orientation_manager_enable(bool on) { + prv_change_orientation(on ? display_orientation_is_left() : false); +} +#endif \ No newline at end of file diff --git a/src/fw/services/normal/orientation_manager.h b/src/fw/services/normal/orientation_manager.h new file mode 100644 index 000000000..155be6b04 --- /dev/null +++ b/src/fw/services/normal/orientation_manager.h @@ -0,0 +1,7 @@ +#if PLATFORM_ASTERIX +#include "shell/prefs.h" + +void orientation_handle_prefs_changed(void); + +void orientation_manager_enable(bool on); +#endif \ No newline at end of file diff --git a/src/fw/services/normal/services_normal.c b/src/fw/services/normal/services_normal.c index db96309c1..32f00639f 100644 --- a/src/fw/services/normal/services_normal.c +++ b/src/fw/services/normal/services_normal.c @@ -47,6 +47,10 @@ #include "services/normal/weather/weather_service.h" #include "services/runlevel_impl.h" +#if PLATFORM_ASTERIX +#include "services/normal/orientation_manager.h" +#endif + #ifndef PLATFORM_TINTIN #include "services/normal/activity/activity.h" #include "services/normal/voice/voice.h" @@ -170,7 +174,13 @@ static struct ServiceRunLevelSetting s_runlevel_settings[] = { { .set_enable_fn = blob_db_enabled, .enable_mask = R_Normal, + }, +#if PLATFORM_ASTERIX + { + .set_enable_fn = orientation_manager_enable, + .enable_mask = R_Stationary | R_Normal, } +#endif }; void services_normal_set_runlevel(RunLevel runlevel) { diff --git a/src/fw/shell/normal/prefs.c b/src/fw/shell/normal/prefs.c index 6d1f06c08..89fe5ea73 100644 --- a/src/fw/shell/normal/prefs.c +++ b/src/fw/shell/normal/prefs.c @@ -35,6 +35,9 @@ #include "services/common/accel_manager.h" #include "services/common/hrm/hrm_manager.h" #include "services/common/i18n/i18n.h" +#if PLATFORM_ASTERIX +#include "services/normal/orientation_manager.h" +#endif #include "services/normal/bluetooth/ble_hrm.h" #include "services/normal/settings/settings_file.h" #include "services/normal/timeline/peek.h" @@ -84,8 +87,19 @@ static uint8_t s_motion_sensitivity = 85; // Default to High #if CAPABILITY_HAS_DYNAMIC_BACKLIGHT #define PREF_KEY_BACKLIGHT_DYNAMIC_INTENSITY "lightDynamicIntensity" static bool s_backlight_dynamic_intensity_enabled = false; + +#define PREF_KEY_DYNAMIC_BACKLIGHT_MIN_THRESHOLD "dynBacklightMinThreshold" +static uint32_t s_dynamic_backlight_min_threshold = 0; // default set from board config in shell_prefs_init() + +#define PREF_KEY_DYNAMIC_BACKLIGHT_MAX_THRESHOLD "dynBacklightMaxThreshold" +static uint32_t s_dynamic_backlight_max_threshold = 0; // default set from board config in shell_prefs_init() #endif +#if PLATFORM_ASTERIX +#define PREF_KEY_DISPLAY_ORIENTATION_LEFT_HANDED "displayOrientationLeftHanded" +static bool s_display_orientation_left = false; +#endif + #define PREF_KEY_BACKLIGHT_AMBIENT_THRESHOLD "lightAmbientThreshold" static uint32_t s_backlight_ambient_threshold = 0; // default set from board config in shell_prefs_init() @@ -304,6 +318,30 @@ static bool prv_set_s_backlight_dynamic_intensity_enabled(bool *enabled) { s_backlight_dynamic_intensity_enabled = *enabled; return true; } + +static bool prv_set_s_dynamic_backlight_min_threshold(uint32_t *threshold) { + // Validate and constrain the threshold + if (*threshold > AMBIENT_LIGHT_LEVEL_MAX) { + s_dynamic_backlight_min_threshold = AMBIENT_LIGHT_LEVEL_MAX; + return false; + } + s_dynamic_backlight_min_threshold = *threshold; + return true; +} + +static bool prv_set_s_dynamic_backlight_max_threshold(uint32_t *threshold) { + // Validate and constrain the threshold + if (*threshold > AMBIENT_LIGHT_LEVEL_MAX) { + s_dynamic_backlight_max_threshold = AMBIENT_LIGHT_LEVEL_MAX; + return false; + } + if (*threshold < 1) { + s_dynamic_backlight_max_threshold = 1; + return false; + } + s_dynamic_backlight_max_threshold = *threshold; + return true; +} #endif static bool prv_set_s_motion_sensitivity(uint8_t *sensitivity) { @@ -316,7 +354,7 @@ static bool prv_set_s_motion_sensitivity(uint8_t *sensitivity) { // Update accelerometer sensitivity in accel_manager // This applies the setting to the hardware - #if PLATFORM_ASTERIX + #if PLATFORM_ASTERIX || PLATFORM_OBELIX accel_manager_update_sensitivity(*sensitivity); #endif @@ -339,6 +377,14 @@ static bool prv_set_s_backlight_ambient_threshold(uint32_t *threshold) { return true; } +#if PLATFORM_ASTERIX +static bool prv_set_s_display_orientation_left(bool *left) { + s_display_orientation_left = *left; + orientation_handle_prefs_changed(); + return true; +} +#endif + static bool prv_set_s_stationary_mode_enabled(bool *enabled) { s_stationary_mode_enabled = *enabled; return true; @@ -636,6 +682,10 @@ void shell_prefs_init(void) { s_backlight_intensity = prv_convert_backlight_percent_to_intensity(BOARD_CONFIG.backlight_on_percent); s_backlight_ambient_threshold = BOARD_CONFIG.ambient_light_dark_threshold; +#if CAPABILITY_HAS_DYNAMIC_BACKLIGHT + s_dynamic_backlight_min_threshold = BOARD_CONFIG.dynamic_backlight_min_threshold; + s_dynamic_backlight_max_threshold = BOARD_CONFIG.dynamic_backlight_max_threshold; +#endif s_mutex = mutex_create(); SettingsFile file = {{0}}; @@ -662,7 +712,7 @@ void shell_prefs_init(void) { ambient_light_set_dark_threshold(s_backlight_ambient_threshold); // Update accelerometer sensitivity with the loaded value -#if PLATFORM_ASTERIX +#if PLATFORM_ASTERIX || PLATFORM_OBELIX accel_manager_update_sensitivity(s_motion_sensitivity); #endif } @@ -961,6 +1011,56 @@ void backlight_set_ambient_threshold(uint32_t threshold) { ambient_light_set_dark_threshold(threshold); } +#if CAPABILITY_HAS_DYNAMIC_BACKLIGHT +uint32_t backlight_get_dynamic_min_threshold(void) { + return s_dynamic_backlight_min_threshold; +} + +void backlight_set_dynamic_min_threshold(uint32_t threshold) { + // Validate threshold is within acceptable range + if (threshold > AMBIENT_LIGHT_LEVEL_MAX) { + threshold = AMBIENT_LIGHT_LEVEL_MAX; + } + if (threshold < 0) { + threshold = 0; + } + // Ensure min threshold is less than max threshold + if (threshold >= s_dynamic_backlight_max_threshold && s_dynamic_backlight_max_threshold > 0) { + threshold = s_dynamic_backlight_max_threshold - 1; + } + prv_pref_set(PREF_KEY_DYNAMIC_BACKLIGHT_MIN_THRESHOLD, &threshold, sizeof(threshold)); +} + +uint32_t backlight_get_dynamic_max_threshold(void) { + return s_dynamic_backlight_max_threshold; +} + +void backlight_set_dynamic_max_threshold(uint32_t threshold) { + // Validate threshold is within acceptable range + if (threshold > AMBIENT_LIGHT_LEVEL_MAX) { + threshold = AMBIENT_LIGHT_LEVEL_MAX; + } + if (threshold < 1) { + threshold = 1; + } + // Ensure max threshold is greater than min threshold + if (threshold <= s_dynamic_backlight_min_threshold) { + threshold = s_dynamic_backlight_min_threshold + 1; + } + prv_pref_set(PREF_KEY_DYNAMIC_BACKLIGHT_MAX_THRESHOLD, &threshold, sizeof(threshold)); +} +#endif + +#if PLATFORM_ASTERIX +bool display_orientation_is_left(void) { + return s_display_orientation_left; +} + +void display_orientation_set_left(bool left) { + prv_pref_set(PREF_KEY_DISPLAY_ORIENTATION_LEFT_HANDED, &left, sizeof(left)); +} +#endif + bool shell_prefs_get_stationary_enabled(void) { return s_stationary_mode_enabled; } diff --git a/src/fw/shell/normal/prefs_values.h.inc b/src/fw/shell/normal/prefs_values.h.inc index 4fe65e7ee..443900ae6 100644 --- a/src/fw/shell/normal/prefs_values.h.inc +++ b/src/fw/shell/normal/prefs_values.h.inc @@ -11,6 +11,8 @@ PREFS_MACRO(PREF_KEY_MOTION_SENSITIVITY, s_motion_sensitivity) #if CAPABILITY_HAS_DYNAMIC_BACKLIGHT PREFS_MACRO(PREF_KEY_BACKLIGHT_DYNAMIC_INTENSITY, s_backlight_dynamic_intensity_enabled) + PREFS_MACRO(PREF_KEY_DYNAMIC_BACKLIGHT_MIN_THRESHOLD, s_dynamic_backlight_min_threshold) + PREFS_MACRO(PREF_KEY_DYNAMIC_BACKLIGHT_MAX_THRESHOLD, s_dynamic_backlight_max_threshold) #endif PREFS_MACRO(PREF_KEY_BACKLIGHT_AMBIENT_THRESHOLD, s_backlight_ambient_threshold) PREFS_MACRO(PREF_KEY_STATIONARY, s_stationary_mode_enabled) @@ -26,6 +28,9 @@ PREFS_MACRO(PREF_KEY_QUICK_LAUNCH_SETUP_OPENED, s_quick_launch_setup_opened) PREFS_MACRO(PREF_KEY_DEFAULT_WATCHFACE, s_default_watchface) PREFS_MACRO(PREF_KEY_WELCOME_VERSION, s_welcome_version) +#if PLATFORM_ASTERIX + PREFS_MACRO(PREF_KEY_DISPLAY_ORIENTATION_LEFT_HANDED, s_display_orientation_left) +#endif #if CAPABILITY_HAS_HEALTH_TRACKING PREFS_MACRO(PREF_KEY_ACTIVITY_PREFERENCES, s_activity_preferences) PREFS_MACRO(PREF_KEY_ACTIVITY_ACTIVATED_TIMESTAMP, s_activity_activation_timestamp) diff --git a/src/fw/shell/prefs.h b/src/fw/shell/prefs.h index 8605638e6..08e67586b 100644 --- a/src/fw/shell/prefs.h +++ b/src/fw/shell/prefs.h @@ -93,6 +93,12 @@ void backlight_set_motion_enabled(bool enable); // Dynamic backlight intensity based on ambient light sensor bool backlight_is_dynamic_intensity_enabled(void); void backlight_set_dynamic_intensity_enabled(bool enable); + +// Dynamic backlight thresholds (for debug menu) +uint32_t backlight_get_dynamic_min_threshold(void); +void backlight_set_dynamic_min_threshold(uint32_t threshold); +uint32_t backlight_get_dynamic_max_threshold(void); +void backlight_set_dynamic_max_threshold(uint32_t threshold); #endif // Motion sensitivity for accelerometer shake detection (0-100, lower = less sensitive) @@ -146,6 +152,11 @@ LegacyAppRenderMode shell_prefs_get_legacy_app_render_mode(void); void shell_prefs_set_legacy_app_render_mode(LegacyAppRenderMode mode); #endif +#if PLATFORM_ASTERIX +bool display_orientation_is_left(void); +void display_orientation_set_left(bool left); +#endif + GColor shell_prefs_get_settings_menu_highlight_color(void); void shell_prefs_set_settings_menu_highlight_color(GColor color); diff --git a/src/fw/wscript b/src/fw/wscript index bfcdee8aa..7344cdfc0 100644 --- a/src/fw/wscript +++ b/src/fw/wscript @@ -155,7 +155,7 @@ def _generate_memory_layout(bld): bootloader_size = 64 * 1024 slot_size = 3072 * 1024 resources_size = 2048 * 1024 - prf_size = 512 * 1024 + prf_size = 576 * 1024 if bld.variant == 'prf' and not (bld.env.IS_MFG or bld.env.PRF_AS_FIRMWARE): offset_size = ptable_size + bootloader_size + 2 * slot_size + 2 * resources_size fw_max_size = prf_size @@ -564,45 +564,19 @@ def _build_recovery(bld): if 'MFG_INFO_RECORDS_TEST_RESULTS' not in bld.env.DEFINES: excludes.extend('mfg/results_ui.c') - if not bld.env.IS_MFG: + if not bld.is_asterix(): excludes.extend([ - 'apps/prf_apps/mfg_accel_app.c', - 'apps/prf_apps/mfg_als_app.c', - 'apps/prf_apps/mfg_audio_app.c', - 'apps/prf_apps/mfg_backlight_app.c', - 'apps/prf_apps/mfg_battery_discharge_app.c', - 'apps/prf_apps/mfg_bt_device_name_app.c', - 'apps/prf_apps/mfg_bt_sig_rf_app_app.c', - 'apps/prf_apps/mfg_button_app.c', - 'apps/prf_apps/mfg_certification_app.c', - 'apps/prf_apps/mfg_display_app.c', - 'apps/prf_apps/mfg_display_calibration_app.c', - 'apps/prf_apps/mfg_hrm_app.c', + 'apps/prf_apps/mfg_speaker_app.c', 'apps/prf_apps/mfg_mic_app.c', + 'apps/prf_apps/mfg_sine_wave.c', + ]) + + if not bld.is_obelix(): + excludes.extend([ + 'apps/prf_apps/mfg_audio_app.c', 'apps/prf_apps/mfg_pdm_mic_app.c', - 'apps/prf_apps/mfg_program_color_app.c', - 'apps/prf_apps/mfg_runin_app.c', - 'apps/prf_apps/mfg_serial_qr_app.c', - 'apps/prf_apps/mfg_sine_wave_app.c', - 'apps/prf_apps/mfg_speaker_app.c', 'apps/prf_apps/mfg_test_aging_app.c', - 'apps/prf_apps/mfg_touch_app.c', - 'apps/prf_apps/mfg_vibe_app.c', ]) - else: - if not bld.is_asterix(): - excludes.extend([ - 'apps/prf_apps/mfg_speaker_app.c', - 'apps/prf_apps/mfg_mic_app.c', - 'apps/prf_apps/mfg_sine_wave.c', - ]) - - if not bld.is_obelix(): - excludes.extend([ - 'apps/prf_apps/mfg_audio_app.c', - 'apps/prf_apps/mfg_pdm_mic_app.c', - 'apps/prf_apps/mfg_test_aging_app.c', - ]) if bld.is_silk(): bld.env.append_value('DEFINES', ['QSPI_DMA_DISABLE=1']) diff --git a/src/include/pebbleos/firmware_metadata.h b/src/include/pebbleos/firmware_metadata.h index bc777f68d..ed2665d21 100644 --- a/src/include/pebbleos/firmware_metadata.h +++ b/src/include/pebbleos/firmware_metadata.h @@ -54,6 +54,7 @@ typedef enum FirmwareMetadataPlatform { FirmwareMetadataPlatformPebbleAsterix = 15, FirmwareMetadataPlatformPebbleObelixEVT = 16, FirmwareMetadataPlatformPebbleObelixDVT = 17, + FirmwareMetadataPlatformPebbleObelixPVT = 18, FirmwareMetadataPlatformPebbleOneBigboard = 0xff, FirmwareMetadataPlatformPebbleOneBigboard2 = 0xfe, diff --git a/third_party/hal_lis2dw12/lis2dw12-pid b/third_party/hal_lis2dw12/lis2dw12-pid new file mode 160000 index 000000000..3fd41f8bf --- /dev/null +++ b/third_party/hal_lis2dw12/lis2dw12-pid @@ -0,0 +1 @@ +Subproject commit 3fd41f8bf20b50855c0c1044a06e32f737029de5 diff --git a/third_party/hal_lis2dw12/wscript b/third_party/hal_lis2dw12/wscript new file mode 100644 index 000000000..3c992b443 --- /dev/null +++ b/third_party/hal_lis2dw12/wscript @@ -0,0 +1,14 @@ +def build(bld): + lis2dw12_includes = [ + 'lis2dw12-pid' + ] + + lis2dw12_sources = [ + 'lis2dw12-pid/lis2dw12_reg.c', + ] + + bld.stlib(source=lis2dw12_sources, + includes=lis2dw12_includes, + export_includes=lis2dw12_includes, + target='hal_lis2dw12', + use=['pblibc']) diff --git a/third_party/speex/os_support_custom.h b/third_party/speex/os_support_custom.h index 5b337968e..c9a9ab04c 100644 --- a/third_party/speex/os_support_custom.h +++ b/third_party/speex/os_support_custom.h @@ -24,15 +24,4 @@ static inline void speex_warning(const char *str) { (void)str; } static inline void speex_warning_int(const char *str, int val) { (void)str; (void)val; } static inline void speex_notify(const char *str) { (void)str; } -/* Disable math functions that aren't available */ -#define sqrt(x) 0 -#define floor(x) ((int)(x)) -#define cos(x) 0 -#define sin(x) 0 -#define exp(x) 1 -#define log(x) 0 -#define pow(x, y) 1 -#define rint(x) ((int)(x)) -#define fabs(x) ((x) < 0 ? -(x) : (x)) - -#endif /* OS_SUPPORT_CUSTOM_H */ \ No newline at end of file +#endif /* OS_SUPPORT_CUSTOM_H */ diff --git a/third_party/wscript b/third_party/wscript index 40ae83b6a..d84b6a7eb 100644 --- a/third_party/wscript +++ b/third_party/wscript @@ -38,4 +38,7 @@ def build(bld): if bld.is_asterix() or bld.is_obelix(): bld.recurse('hal_lsm6dso') + if bld.is_obelix(): + bld.recurse('hal_lis2dw12') + bld.recurse('memfault') diff --git a/wscript b/wscript index c6c282a0c..6b7707bd6 100644 --- a/wscript +++ b/wscript @@ -54,6 +54,7 @@ RUNNERS = { 'asterix': ['openocd', 'nrfutil'], 'obelix_evt': ['sftool'], 'obelix_dvt': ['sftool'], + 'obelix_pvt': ['sftool'], 'obelix_bb': ['sftool'], 'obelix_bb2': ['sftool'], } @@ -119,6 +120,7 @@ def options(opt): 'asterix', 'obelix_evt', 'obelix_dvt', + 'obelix_pvt', 'obelix_bb', 'obelix_bb2', ], @@ -1659,7 +1661,7 @@ def _check_firmware_image_size(ctx, path): max_firmware_size = (1024 - 32) * BYTES_PER_K elif ctx.env.MICRO_FAMILY == 'SF32LB52': if ctx.variant == 'prf' and not ctx.env.IS_MFG: - max_firmware_size = 512 * BYTES_PER_K + max_firmware_size = 576 * BYTES_PER_K else: # 3072k of flash max_firmware_size = 3072 * BYTES_PER_K