diff --git a/samples/bluetooth/fast_pair/locator_tag/src/motion_detector/CMakeLists.txt b/samples/bluetooth/fast_pair/locator_tag/src/motion_detector/CMakeLists.txt index 6fcb02cbad1c..b0d0271baafa 100644 --- a/samples/bluetooth/fast_pair/locator_tag/src/motion_detector/CMakeLists.txt +++ b/samples/bluetooth/fast_pair/locator_tag/src/motion_detector/CMakeLists.txt @@ -8,4 +8,9 @@ zephyr_library_named(app_motion_detector) target_include_directories(app_motion_detector PUBLIC include) -target_sources(app_motion_detector PRIVATE platform_default.c) +if(CONFIG_APP_PLATFORM_DK) + target_sources(app_motion_detector PRIVATE platform_dk.c) + target_link_libraries(app_motion_detector PRIVATE app_ui) +else() + target_sources(app_motion_detector PRIVATE platform_default.c) +endif() diff --git a/samples/bluetooth/fast_pair/locator_tag/src/motion_detector/platform_dk.c b/samples/bluetooth/fast_pair/locator_tag/src/motion_detector/platform_dk.c new file mode 100644 index 000000000000..fb9a7d59f8c1 --- /dev/null +++ b/samples/bluetooth/fast_pair/locator_tag/src/motion_detector/platform_dk.c @@ -0,0 +1,79 @@ +/* + * Copyright (c) 2024 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause + */ + +#include + +#include + +#include "app_motion_detector.h" +#include "app_ui.h" + +#include +LOG_MODULE_DECLARE(fp_fmdn, LOG_LEVEL_INF); + +static bool motion_detector_active; +static bool motion_detected; + +static void fmdn_motion_detector_start(void) +{ + __ASSERT_NO_MSG(!motion_detector_active); + LOG_INF("FMDN: motion detector started"); + motion_detector_active = true; + app_ui_state_change_indicate(APP_UI_STATE_MOTION_DETECTOR_ACTIVE, motion_detector_active); +} + +static bool fmdn_motion_detector_period_expired(void) +{ + bool ret = motion_detected; + + __ASSERT_NO_MSG(motion_detector_active); + motion_detected = false; + LOG_INF("FMDN: motion detector period expired. Reporting that the motion was %sdetected", + ret ? "" : "not "); + return ret; +} + +static void fmdn_motion_detector_stop(void) +{ + __ASSERT_NO_MSG(motion_detector_active); + LOG_INF("FMDN: motion detector stopped"); + motion_detector_active = false; + app_ui_state_change_indicate(APP_UI_STATE_MOTION_DETECTOR_ACTIVE, motion_detector_active); + motion_detected = false; +} + +static const struct bt_fast_pair_fmdn_motion_detector_cb fmdn_motion_detector_cb = { + .start = fmdn_motion_detector_start, + .period_expired = fmdn_motion_detector_period_expired, + .stop = fmdn_motion_detector_stop, +}; + +static void motion_detector_request_handle(enum app_ui_request request) +{ + if (request == APP_UI_REQUEST_MOTION_INDICATE) { + if (motion_detector_active) { + LOG_INF("FMDN: motion indicated"); + motion_detected = true; + } else { + LOG_INF("FMDN: motion indicated: motion detector is inactive"); + } + } +} + +int app_motion_detector_init(void) +{ + int err; + + err = bt_fast_pair_fmdn_motion_detector_cb_register(&fmdn_motion_detector_cb); + if (err) { + LOG_ERR("FMDN: bt_fast_pair_fmdn_motion_detector_cb_register failed (err %d)", err); + return err; + } + + return 0; +} + +APP_UI_REQUEST_LISTENER_REGISTER(motion_detector_request_handler, motion_detector_request_handle); diff --git a/samples/bluetooth/fast_pair/locator_tag/src/ui/include/app_ui.h b/samples/bluetooth/fast_pair/locator_tag/src/ui/include/app_ui.h index 397b620cab2e..2de84913da9e 100644 --- a/samples/bluetooth/fast_pair/locator_tag/src/ui/include/app_ui.h +++ b/samples/bluetooth/fast_pair/locator_tag/src/ui/include/app_ui.h @@ -33,6 +33,7 @@ enum app_ui_state { APP_UI_STATE_PROVISIONED, APP_UI_STATE_FP_ADV, APP_UI_STATE_DFU_MODE, + APP_UI_STATE_MOTION_DETECTOR_ACTIVE, APP_UI_STATE_COUNT, }; @@ -60,6 +61,7 @@ enum app_ui_request { APP_UI_REQUEST_FP_ADV_MODE_CHANGE, APP_UI_REQUEST_FACTORY_RESET, APP_UI_REQUEST_DFU_MODE_ENTER, + APP_UI_REQUEST_MOTION_INDICATE, APP_UI_REQUEST_COUNT, }; diff --git a/samples/bluetooth/fast_pair/locator_tag/src/ui/platform_dk.c b/samples/bluetooth/fast_pair/locator_tag/src/ui/platform_dk.c index d793fe8edcb4..4461fd93a475 100644 --- a/samples/bluetooth/fast_pair/locator_tag/src/ui/platform_dk.c +++ b/samples/bluetooth/fast_pair/locator_tag/src/ui/platform_dk.c @@ -21,6 +21,9 @@ LOG_MODULE_DECLARE(fp_fmdn, LOG_LEVEL_DBG); /* Minimum button hold time in milliseconds to trigger the DFU mode. */ #define DFU_MODE_BTN_MIN_HOLD_TIME_MS 7000 +/* Maximum time between two subsequent button clicks to consider them as double click action. */ +#define BTN_DOUBLE_CLICK_TIMEOUT_MS 500 + /* Run status LED blinking interval. */ #define RUN_LED_BLINK_INTERVAL_MS 1000 @@ -39,13 +42,25 @@ LOG_MODULE_DECLARE(fp_fmdn, LOG_LEVEL_DBG); /* Provisioning LED blinking interval when not provisioned and FP advertising is active. */ #define PROVISIONING_LED_FP_ADV_NOT_PROV_BLINK_INTERVAL_MS 1000 +/* Ring and motion status LED blinking interval when motion detector is active. */ +#define RING_AND_MOTION_LED_MOTION_DETECTOR_ACTIVE_BLINK_INTERVAL_MS 250 + +/* Ring and motion status LED blinking interval when indicating motion event. */ +#define RING_AND_MOTION_LED_MOTION_INDICATE_BLINK_INTERVAL_MS 100 + +/* Ring and motion status LED maximum toggle count when indicating motion event. */ +#define RING_AND_MOTION_LED_MOTION_INDICATE_TOGGLE_COUNT_MAX 4 + +/* Toggle count should be even to make a LED return to its initial state. */ +BUILD_ASSERT((RING_AND_MOTION_LED_MOTION_INDICATE_TOGGLE_COUNT_MAX % 2) == 0); + /* Assignments of the DK LEDs and buttons to different functionalities and modules. */ /* Run and DFU mode status LED. */ #define APP_RUN_STATUS_LED DK_LED1 -/* Ringing status LED. */ -#define APP_RING_LED DK_LED2 +/* Ringing and motion status LED. */ +#define APP_RING_AND_MOTION_LED DK_LED2 /* Provisioning and FP advertising status LED. */ #define APP_PROVISIONING_LED DK_LED3 @@ -56,8 +71,8 @@ LOG_MODULE_DECLARE(fp_fmdn, LOG_LEVEL_DBG); /* Button used to change the Fast Pair advertising mode. */ #define APP_FP_ADV_MODE_BTN DK_BTN1_MSK -/* Button used to stop the ringing action. */ -#define APP_RING_BTN DK_BTN2_MSK +/* Button used to stop the ringing action on single click and indicate motion on double click. */ +#define APP_RING_AND_MOTION_BTN DK_BTN2_MSK /* Button used to change the battery level. */ #define APP_BATTERY_BTN DK_BTN3_MSK @@ -86,6 +101,22 @@ static K_WORK_DELAYABLE_DEFINE(mode_led_work, mode_led_work_handle); static void provisioning_led_work_handle(struct k_work *item); static K_WORK_DELAYABLE_DEFINE(provisioning_led_work, provisioning_led_work_handle); +static void ring_and_motion_led_work_handle(struct k_work *item); +static K_WORK_DELAYABLE_DEFINE(ring_and_motion_state_led_work, ring_and_motion_led_work_handle); +/* This work object is different from the previous work items used to signal the UI state as it is + * used to signal the UI request. + */ +static K_WORK_DELAYABLE_DEFINE(motion_indicate_led_work, ring_and_motion_led_work_handle); +static struct { + struct k_work_delayable *work; + bool signal_start; +} motion_indicate_led = { + .work = &motion_indicate_led_work, +}; + +static void ring_stop_work_handle(struct k_work *item); +static K_WORK_DELAYABLE_DEFINE(ring_stop_work, ring_stop_work_handle); + static struct { struct k_work_delayable *work; const uint32_t displayed_state_bm; @@ -95,6 +126,9 @@ static struct { BIT(APP_UI_STATE_RECOVERY_MODE))}, {.work = &provisioning_led_work, .displayed_state_bm = (BIT(APP_UI_STATE_PROVISIONED) | BIT(APP_UI_STATE_FP_ADV))}, + {.work = &ring_and_motion_state_led_work, + .displayed_state_bm = (BIT(APP_UI_STATE_RINGING) | + BIT(APP_UI_STATE_MOTION_DETECTOR_ACTIVE))}, }; static ATOMIC_DEFINE(ui_state_status, APP_UI_STATE_COUNT); @@ -105,6 +139,10 @@ BUILD_ASSERT(DFU_MODE_BTN_MIN_HOLD_TIME_MS > RECOVERY_MODE_BTN_MIN_HOLD_TIME_MS) static void btn_handle(uint32_t button_state, uint32_t has_changed) { + /* It is assumed that this function executes in the cooperative thread context. */ + __ASSERT_NO_MSG(!k_is_preempt_thread()); + __ASSERT_NO_MSG(!k_is_in_isr()); + if (has_changed & APP_MODE_CTLR_BTN) { static int64_t prev_uptime; @@ -138,8 +176,30 @@ static void btn_handle(uint32_t button_state, uint32_t has_changed) app_ui_request_broadcast(APP_UI_REQUEST_FP_ADV_MODE_CHANGE); } - if (has_changed & button_state & APP_RING_BTN) { - app_ui_request_broadcast(APP_UI_REQUEST_RINGING_STOP); + if (has_changed & button_state & APP_RING_AND_MOTION_BTN) { + if (k_work_delayable_is_pending(&ring_stop_work)) { + /* Double click */ + int ret; + + ret = k_work_cancel_delayable(&ring_stop_work); + __ASSERT_NO_MSG(!ret); + + app_ui_request_broadcast(APP_UI_REQUEST_MOTION_INDICATE); + + /* The motion indicate signalization is triggered from inside of this module + * contrary to state change signalizations which are triggered by outside + * moudules via app_ui_state_change_indicate function. + */ + motion_indicate_led.signal_start = true; + (void) k_work_reschedule_for_queue(&led_workq, motion_indicate_led.work, + K_NO_WAIT); + } else { + /* First click - wait for a second click and stop ringing if it doesn't + * appear in time. + */ + (void) k_work_schedule(&ring_stop_work, + K_MSEC(BTN_DOUBLE_CLICK_TIMEOUT_MS)); + } } if (has_changed & button_state & APP_BATTERY_BTN) { @@ -147,6 +207,11 @@ static void btn_handle(uint32_t button_state, uint32_t has_changed) } } +static void ring_stop_work_handle(struct k_work *item) +{ + app_ui_request_broadcast(APP_UI_REQUEST_RINGING_STOP); +} + static void bootup_btn_handle(void) { uint32_t button_state; @@ -223,6 +288,78 @@ static void provisioning_led_work_handle(struct k_work *item) dk_set_led(APP_PROVISIONING_LED, provisioning_led_on); } +static bool ring_and_motion_state_led_handle(bool led_on) +{ + if (atomic_test_bit(ui_state_status, APP_UI_STATE_RINGING)) { + /* Ringing takes precedence over motion detector active state. */ + led_on = true; + } else if (atomic_test_bit(ui_state_status, APP_UI_STATE_MOTION_DETECTOR_ACTIVE)) { + led_on = !led_on; + (void) k_work_reschedule_for_queue( + &led_workq, + &ring_and_motion_state_led_work, + K_MSEC(RING_AND_MOTION_LED_MOTION_DETECTOR_ACTIVE_BLINK_INTERVAL_MS)); + } else { + led_on = false; + } + + return led_on; +} + +static bool motion_indicate_led_handle(bool led_on) +{ + static size_t toggle_count; + + if (motion_indicate_led.signal_start) { + toggle_count = 0; + motion_indicate_led.signal_start = false; + } + + led_on = !led_on; + toggle_count++; + + if (toggle_count < RING_AND_MOTION_LED_MOTION_INDICATE_TOGGLE_COUNT_MAX) { + (void) k_work_reschedule_for_queue( + &led_workq, + motion_indicate_led.work, + K_MSEC(RING_AND_MOTION_LED_MOTION_INDICATE_BLINK_INTERVAL_MS)); + } else { + /* Signal correct ringing and motion state after signalizing motion indication + * finishes. + */ + (void) k_work_reschedule_for_queue( + &led_workq, + &ring_and_motion_state_led_work, + K_MSEC(RING_AND_MOTION_LED_MOTION_INDICATE_BLINK_INTERVAL_MS)); + } + + return led_on; +} + +static void ring_and_motion_led_work_handle(struct k_work *item) +{ + struct k_work_delayable *dwork = k_work_delayable_from_work(item); + static bool ring_and_motion_led_on; + + if (dwork == &ring_and_motion_state_led_work) { + if (k_work_delayable_is_pending(motion_indicate_led.work)) { + /* Do nothing - signalizing motion indication has higher priority. Proper + * ringing and motion state will be signalized after signalizing motion + * indication finishes. + */ + return; + } + + ring_and_motion_led_on = ring_and_motion_state_led_handle(ring_and_motion_led_on); + } else if (dwork == motion_indicate_led.work) { + ring_and_motion_led_on = motion_indicate_led_handle(ring_and_motion_led_on); + } else { + __ASSERT_NO_MSG(false); + } + + dk_set_led(APP_RING_AND_MOTION_LED, ring_and_motion_led_on); +} + int app_ui_state_change_indicate(enum app_ui_state state, bool active) { __ASSERT_NO_MSG(state < APP_UI_STATE_COUNT); @@ -237,13 +374,6 @@ int app_ui_state_change_indicate(enum app_ui_state state, bool active) } } - /* Only the ring LED needs to be driven here directly, the remaining LEDs - * are handled by the respective work items. - */ - if (state == APP_UI_STATE_RINGING) { - dk_set_led(APP_RING_LED, active); - } - return 0; }