Skip to content

Commit

Permalink
samples: bluetooth: fast_pair: locator_tag: Add motion detector UI in DK
Browse files Browse the repository at this point in the history
Improves motion detector integration in the Locator Tag sample for DKs.
Double click of Button 2 indicates detected motion.
The detected motion is signalized on LED2 by two fast blinks and
reported to the FMDN stack if motion detector is enabled.
The motion detector active state is signalized on LED2 by blinking
with medium interval.

Jira: NCSDK-28952

Signed-off-by: Aleksander Strzebonski <[email protected]>
  • Loading branch information
alstrzebonski authored and rlubos committed Oct 3, 2024
1 parent c2300ba commit 9be2617
Show file tree
Hide file tree
Showing 4 changed files with 230 additions and 14 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/*
* Copyright (c) 2024 Nordic Semiconductor ASA
*
* SPDX-License-Identifier: LicenseRef-Nordic-5-Clause
*/

#include <zephyr/kernel.h>

#include <bluetooth/services/fast_pair/fmdn.h>

#include "app_motion_detector.h"
#include "app_ui.h"

#include <zephyr/logging/log.h>
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);
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
Expand Down Expand Up @@ -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,
};
Expand Down
156 changes: 143 additions & 13 deletions samples/bluetooth/fast_pair/locator_tag/src/ui/platform_dk.c
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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;
Expand All @@ -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);
Expand All @@ -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;

Expand Down Expand Up @@ -138,15 +176,42 @@ 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) {
app_ui_request_broadcast(APP_UI_REQUEST_SIMULATED_BATTERY_CHANGE);
}
}

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;
Expand Down Expand Up @@ -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);
Expand All @@ -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;
}

Expand Down

0 comments on commit 9be2617

Please sign in to comment.