sensors: Create `sensor trig` subcommand

This PR relocates the sensor trigger sample application from the
`sensor_shell` sample to a subcommand in the actual sensor shell. The
subcommand has a UI for enabling a given trigger on a given sensor.
A built-in handler for the data_ready trigger is included that prints
the latest data to the log. Currently, only `SENSOR_TRIG_DATA_READY` is
supported but the groundwork is there to add others. Tested on a
`tdk_robokit1` board.

Signed-off-by: Tristan Honscheid <honscheid@google.com>
diff --git a/drivers/sensor/Kconfig b/drivers/sensor/Kconfig
index 9f94ff1..838bdf1 100644
--- a/drivers/sensor/Kconfig
+++ b/drivers/sensor/Kconfig
@@ -44,6 +44,14 @@
 	  in a convenient format. It makes use of a fuel gauge to read its
 	  information.
 
+config SENSOR_SHELL_TRIG_PRINT_TIMEOUT_MS
+	int "Timeout for printing the average sensor sample value"
+	default 5000
+	depends on SENSOR_SHELL
+	help
+	  Control the frequency of the sampling window over which the sensor
+	  interrupt handler will collect data.
+
 config SENSOR_INFO
 	bool "Sensor Info iterable section"
 
diff --git a/drivers/sensor/sensor_shell.c b/drivers/sensor/sensor_shell.c
index d58c0a8..b8c8282 100644
--- a/drivers/sensor/sensor_shell.c
+++ b/drivers/sensor/sensor_shell.c
@@ -14,6 +14,8 @@
 #include <zephyr/shell/shell.h>
 #include <zephyr/sys/iterable_sections.h>
 
+LOG_MODULE_REGISTER(sensor_shell);
+
 #define SENSOR_GET_HELP                                                                            \
 	"Get sensor data. Channel names are optional. All channels are read "                      \
 	"when no channels are provided. Syntax:\n"                                                 \
@@ -30,6 +32,10 @@
 
 #define SENSOR_INFO_HELP "Get sensor info, such as vendor and model name, for all sensors."
 
+#define SENSOR_TRIG_HELP                                                                           \
+	"Get or set the trigger type on a sensor. Currently only supports `data_ready`.\n"         \
+	"<device_name> <on/off> <trigger_name>"
+
 const char *sensor_channel_name[SENSOR_CHAN_ALL] = {
 	[SENSOR_CHAN_ACCEL_X] = "accel_x",
 	[SENSOR_CHAN_ACCEL_Y] = "accel_y",
@@ -108,6 +114,38 @@
 	[SENSOR_ATTR_FF_DUR] = "ff_dur",
 };
 
+/* Forward declaration */
+static void data_ready_trigger_handler(const struct device *sensor,
+				       const struct sensor_trigger *trigger);
+
+#define TRIGGER_DATA_ENTRY(trig_enum, str_name, handler_func)                                      \
+	[(trig_enum)] = {.name = #str_name,                                                        \
+			 .handler = (handler_func),                                                \
+			 .trigger = {.chan = SENSOR_CHAN_ALL, .type = (trig_enum)}}
+
+/**
+ * @brief This table stores a mapping of string trigger names along with the sensor_trigger struct
+ * that gets passed to the driver to enable that trigger, plus a function pointer to a handler. If
+ * that pointer is NULL, this indicates there is not currently support for that trigger type in the
+ * sensor shell.
+ */
+static const struct {
+	const char *name;
+	sensor_trigger_handler_t handler;
+	struct sensor_trigger trigger;
+} sensor_trigger_table[SENSOR_TRIG_COMMON_COUNT] = {
+	TRIGGER_DATA_ENTRY(SENSOR_TRIG_TIMER, timer, NULL),
+	TRIGGER_DATA_ENTRY(SENSOR_TRIG_DATA_READY, data_ready, data_ready_trigger_handler),
+	TRIGGER_DATA_ENTRY(SENSOR_TRIG_DELTA, delta, NULL),
+	TRIGGER_DATA_ENTRY(SENSOR_TRIG_NEAR_FAR, near_far, NULL),
+	TRIGGER_DATA_ENTRY(SENSOR_TRIG_THRESHOLD, threshold, NULL),
+	TRIGGER_DATA_ENTRY(SENSOR_TRIG_TAP, tap, NULL),
+	TRIGGER_DATA_ENTRY(SENSOR_TRIG_DOUBLE_TAP, double_tap, NULL),
+	TRIGGER_DATA_ENTRY(SENSOR_TRIG_FREEFALL, freefall, NULL),
+	TRIGGER_DATA_ENTRY(SENSOR_TRIG_MOTION, motion, NULL),
+	TRIGGER_DATA_ENTRY(SENSOR_TRIG_STATIONARY, stationary, NULL),
+};
+
 enum dynamic_command_context {
 	NONE,
 	CTX_GET,
@@ -502,6 +540,61 @@
 }
 SHELL_DYNAMIC_CMD_CREATE(dsub_device_name_for_attr, device_name_get_for_attr);
 
+static void trigger_name_get(size_t idx, struct shell_static_entry *entry)
+{
+	int cnt = 0;
+
+	entry->syntax = NULL;
+	entry->handler = NULL;
+	entry->help = NULL;
+	entry->subcmd = NULL;
+
+	for (int i = 0; i < SENSOR_TRIG_COMMON_COUNT; i++) {
+		if (sensor_trigger_table[i].name != NULL) {
+			if (cnt == idx) {
+				entry->syntax = sensor_trigger_table[i].name;
+				break;
+			}
+			cnt++;
+		}
+	}
+}
+
+SHELL_DYNAMIC_CMD_CREATE(dsub_trigger_name, trigger_name_get);
+
+static void trigger_on_off_get(size_t idx, struct shell_static_entry *entry)
+{
+	entry->handler = NULL;
+	entry->help = NULL;
+	entry->subcmd = &dsub_trigger_name;
+
+	switch (idx) {
+	case 0:
+		entry->syntax = "on";
+		break;
+	case 1:
+		entry->syntax = "off";
+		break;
+	default:
+		entry->syntax = NULL;
+		break;
+	}
+}
+
+SHELL_DYNAMIC_CMD_CREATE(dsub_trigger_onoff, trigger_on_off_get);
+
+static void device_name_get_for_trigger(size_t idx, struct shell_static_entry *entry)
+{
+	const struct device *dev = shell_device_lookup(idx, NULL);
+
+	entry->syntax = (dev != NULL) ? dev->name : NULL;
+	entry->handler = NULL;
+	entry->help = NULL;
+	entry->subcmd = &dsub_trigger_onoff;
+}
+
+SHELL_DYNAMIC_CMD_CREATE(dsub_trigger, device_name_get_for_trigger);
+
 static int cmd_get_sensor_info(const struct shell *sh, size_t argc, char **argv)
 {
 	ARG_UNUSED(argc);
@@ -525,6 +618,119 @@
 #endif
 }
 
+enum sample_stats_state {
+	SAMPLE_STATS_STATE_UNINITIALIZED = 0,
+	SAMPLE_STATS_STATE_ENABLED,
+	SAMPLE_STATS_STATE_DISABLED,
+};
+
+struct sample_stats {
+	int64_t accumulator;
+	uint32_t count;
+	uint64_t sample_window_start;
+	enum sample_stats_state state;
+};
+
+static void data_ready_trigger_handler(const struct device *sensor,
+				       const struct sensor_trigger *trigger)
+{
+	static struct sample_stats stats[SENSOR_CHAN_ALL];
+	const int64_t now = k_uptime_get();
+	struct sensor_value value;
+
+	if (sensor_sample_fetch(sensor)) {
+		LOG_ERR("Failed to fetch samples on data ready handler");
+	}
+	for (int i = 0; i < SENSOR_CHAN_ALL; ++i) {
+		int rc;
+
+		/* Skip disabled channels */
+		if (stats[i].state == SAMPLE_STATS_STATE_DISABLED) {
+			continue;
+		}
+		/* Skip 3 axis channels */
+		if (i == SENSOR_CHAN_ACCEL_XYZ || i == SENSOR_CHAN_GYRO_XYZ ||
+		    i == SENSOR_CHAN_MAGN_XYZ) {
+			continue;
+		}
+
+		rc = sensor_channel_get(sensor, i, &value);
+		if (rc == -ENOTSUP && stats[i].state == SAMPLE_STATS_STATE_UNINITIALIZED) {
+			/* Stop reading this channel if the driver told us it's not supported. */
+			stats[i].state = SAMPLE_STATS_STATE_DISABLED;
+		}
+		if (rc != 0) {
+			/* Skip on any error. */
+			continue;
+		}
+		/* Do something with the data */
+		stats[i].accumulator += value.val1 * INT64_C(1000000) + value.val2;
+		if (stats[i].count++ == 0) {
+			stats[i].sample_window_start = now;
+		} else if (now > stats[i].sample_window_start +
+					 CONFIG_SENSOR_SHELL_TRIG_PRINT_TIMEOUT_MS) {
+			int64_t micro_value = stats[i].accumulator / stats[i].count;
+
+			value.val1 = micro_value / 1000000;
+			value.val2 = (int32_t)llabs(micro_value - (value.val1 * 1000000));
+			LOG_INF("chan=%d, num_samples=%u, data=%d.%06d", i, stats[i].count,
+				value.val1, value.val2);
+
+			stats[i].accumulator = 0;
+			stats[i].count = 0;
+		}
+	}
+}
+
+static int cmd_trig_sensor(const struct shell *sh, size_t argc, char **argv)
+{
+	const struct device *dev;
+	enum sensor_trigger_type trigger;
+	int err;
+
+	if (argc < 4) {
+		shell_error(sh, "Wrong number of args");
+		return -EINVAL;
+	}
+
+	/* Parse device name */
+	dev = device_get_binding(argv[1]);
+	if (dev == NULL) {
+		shell_error(sh, "Device unknown (%s)", argv[1]);
+		return -ENODEV;
+	}
+
+	/* Map the trigger string to an enum value */
+	for (trigger = 0; trigger < ARRAY_SIZE(sensor_trigger_table); trigger++) {
+		if (strcmp(argv[3], sensor_trigger_table[trigger].name) == 0) {
+			break;
+		}
+	}
+	if (trigger >= SENSOR_TRIG_COMMON_COUNT || sensor_trigger_table[trigger].handler == NULL) {
+		shell_error(sh, "Unsupported trigger type (%s)", argv[3]);
+		return -ENOTSUP;
+	}
+
+	/* Parse on/off */
+	if (strcmp(argv[2], "on") == 0) {
+		err = sensor_trigger_set(dev, &sensor_trigger_table[trigger].trigger,
+					 sensor_trigger_table[trigger].handler);
+	} else if (strcmp(argv[2], "off") == 0) {
+		/* Clear the handler for the given trigger on this device */
+		err = sensor_trigger_set(dev, &sensor_trigger_table[trigger].trigger, NULL);
+	} else {
+		shell_error(sh, "Pass 'on' or 'off' to enable/disable trigger");
+		return -EINVAL;
+	}
+
+	if (err) {
+		shell_error(sh, "Error while setting trigger %d on device %s (%d)", trigger,
+			    argv[1], err);
+	}
+
+	return err;
+}
+
 /* clang-format off */
 SHELL_STATIC_SUBCMD_SET_CREATE(sub_sensor,
 	SHELL_CMD_ARG(get, &dsub_device_name, SENSOR_GET_HELP, cmd_get_sensor,
@@ -535,6 +741,8 @@
 			cmd_sensor_attr_get, 2, 255),
 	SHELL_COND_CMD(CONFIG_SENSOR_INFO, info, NULL, SENSOR_INFO_HELP,
 			cmd_get_sensor_info),
+	SHELL_CMD_ARG(trig, &dsub_trigger, SENSOR_TRIG_HELP, cmd_trig_sensor,
+			2, 255),
 	SHELL_SUBCMD_SET_END
 	);
 /* clang-format on */
diff --git a/samples/sensor/sensor_shell/CMakeLists.txt b/samples/sensor/sensor_shell/CMakeLists.txt
index b33730c..1f13027 100644
--- a/samples/sensor/sensor_shell/CMakeLists.txt
+++ b/samples/sensor/sensor_shell/CMakeLists.txt
@@ -6,6 +6,5 @@
 project(sensor_shell)
 
 target_sources(app PRIVATE src/main.c)
-target_sources_ifdef(CONFIG_INIT_TRIG_DATA_READY app PRIVATE src/trigger.c)
 
 target_include_directories(app PRIVATE include)
diff --git a/samples/sensor/sensor_shell/Kconfig b/samples/sensor/sensor_shell/Kconfig
index 370c79d..354385e 100644
--- a/samples/sensor/sensor_shell/Kconfig
+++ b/samples/sensor/sensor_shell/Kconfig
@@ -1,17 +1,4 @@
 # Copyright (c) 2023 Google LLC
 # SPDX-License-Identifier: Apache-2.0
 
-config SAMPLE_PRINT_TIMEOUT_MS
-	int "Timeout for printing the average sensor sample value"
-	default 5000
-	help
-	  Control the frequency of the sampling window over which the sensor
-	  interrupt handler will collect data.
-
 source "Kconfig.zephyr"
-
-config INIT_TRIG_DATA_READY
-	bool "Register data ready triggers for all sensors on start"
-	help
-	  When the application starts, automatically register data ready trigger
-	  listeners to all available sensors.
diff --git a/samples/sensor/sensor_shell/src/main.c b/samples/sensor/sensor_shell/src/main.c
index 31065c6..d8d77c1 100644
--- a/samples/sensor/sensor_shell/src/main.c
+++ b/samples/sensor/sensor_shell/src/main.c
@@ -4,28 +4,12 @@
  * SPDX-License-Identifier: Apache-2.0
  */
 
-#include <stdlib.h>
-#include <zephyr/drivers/sensor.h>
-#include <zephyr/kernel.h>
-#include <zephyr/logging/log.h>
-#include <zephyr/sys/iterable_sections.h>
-
-#include "trigger.h"
-
-LOG_MODULE_REGISTER(app);
+/*
+ * This sample app launches a shell. Interact with it using the `sensor` command. See
+ * `drivers/sensor/sensor_shell.c`. There is nothing to do in the main thread.
+ */
 
 int main(void)
 {
-	if (IS_ENABLED(CONFIG_INIT_TRIG_DATA_READY)) {
-		STRUCT_SECTION_FOREACH(sensor_info, sensor)
-		{
-			struct sensor_trigger trigger = {
-				.chan = SENSOR_CHAN_ALL,
-				.type = SENSOR_TRIG_DATA_READY,
-			};
-			sensor_trigger_set(sensor->dev, &trigger,
-					   sensor_shell_data_ready_trigger_handler);
-		}
-	}
 	return 0;
 }
diff --git a/samples/sensor/sensor_shell/src/trigger.c b/samples/sensor/sensor_shell/src/trigger.c
deleted file mode 100644
index ee92723..0000000
--- a/samples/sensor/sensor_shell/src/trigger.c
+++ /dev/null
@@ -1,78 +0,0 @@
-/*
- * Copyright (c) 2023 Google LLC
- *
- * SPDX-License-Identifier: Apache-2.0
- */
-
-#include "trigger.h"
-
-#include <zephyr/device.h>
-#include <zephyr/drivers/sensor.h>
-#include <zephyr/logging/log.h>
-
-LOG_MODULE_DECLARE(app);
-
-enum sample_stats_state {
-	SAMPLE_STATS_STATE_UNINITIALIZED = 0,
-	SAMPLE_STATS_STATE_ENABLED,
-	SAMPLE_STATS_STATE_DISABLED,
-};
-
-struct sample_stats {
-	int64_t accumulator;
-	uint32_t count;
-	uint64_t sample_window_start;
-	enum sample_stats_state state;
-};
-
-void sensor_shell_data_ready_trigger_handler(const struct device *sensor,
-					     const struct sensor_trigger *trigger)
-{
-	static struct sample_stats stats[SENSOR_CHAN_ALL];
-	const int64_t now = k_uptime_get();
-	struct sensor_value value;
-
-	ARG_UNUSED(trigger);
-
-	if (sensor_sample_fetch(sensor)) {
-		LOG_ERR("Failed to fetch samples on data ready handler");
-	}
-	for (int i = 0; i < SENSOR_CHAN_ALL; ++i) {
-		int rc;
-
-		/* Skip disabled channels */
-		if (stats[i].state == SAMPLE_STATS_STATE_DISABLED) {
-			continue;
-		}
-		/* Skip 3 axis channels */
-		if (i == SENSOR_CHAN_ACCEL_XYZ || i == SENSOR_CHAN_GYRO_XYZ ||
-		    i == SENSOR_CHAN_MAGN_XYZ) {
-			continue;
-		}
-
-		rc = sensor_channel_get(sensor, i, &value);
-		if (rc == -ENOTSUP && stats[i].state == SAMPLE_STATS_STATE_UNINITIALIZED) {
-			/* Stop reading this channel if the driver told us it's not supported. */
-			stats[i].state = SAMPLE_STATS_STATE_DISABLED;
-		}
-		if (rc != 0) {
-			/* Skip on any error. */
-			continue;
-		}
-		/* Do something with the data */
-		stats[i].accumulator += value.val1 * INT64_C(1000000) + value.val2;
-		if (stats[i].count++ == 0) {
-			stats[i].sample_window_start = now;
-		} else if (now > stats[i].sample_window_start + CONFIG_SAMPLE_PRINT_TIMEOUT_MS) {
-			int64_t micro_value = stats[i].accumulator / stats[i].count;
-
-			value.val1 = micro_value / 1000000;
-			value.val2 = (int32_t)llabs(micro_value - (value.val1 * 1000000));
-			LOG_INF("chan=%d, num_samples=%u, data=%d.%06d", i, stats[i].count,
-				value.val1, value.val2);
-
-			stats[i].accumulator = 0;
-			stats[i].count = 0;
-		}
-	}
-}