diff --git a/drivers/video/CMakeLists.txt b/drivers/video/CMakeLists.txt index 102a841648e9..96529ddfa75f 100644 --- a/drivers/video/CMakeLists.txt +++ b/drivers/video/CMakeLists.txt @@ -8,6 +8,7 @@ zephyr_library_sources(video_device.c) zephyr_library_sources_ifdef(CONFIG_VIDEO_MCUX_CSI video_mcux_csi.c) zephyr_library_sources_ifdef(CONFIG_VIDEO_MCUX_MIPI_CSI2RX video_mcux_mipi_csi2rx.c) +zephyr_library_sources_ifdef(CONFIG_VIDEO_SHELL video_shell.c) zephyr_library_sources_ifdef(CONFIG_VIDEO_SW_GENERATOR video_sw_generator.c) zephyr_library_sources_ifdef(CONFIG_VIDEO_MT9M114 mt9m114.c) zephyr_library_sources_ifdef(CONFIG_VIDEO_OV7725 ov7725.c) diff --git a/drivers/video/Kconfig b/drivers/video/Kconfig index a3b163443628..3ab5bb8da182 100644 --- a/drivers/video/Kconfig +++ b/drivers/video/Kconfig @@ -56,6 +56,8 @@ source "drivers/video/Kconfig.mcux_csi" source "drivers/video/Kconfig.mcux_mipi_csi2rx" +source "drivers/video/Kconfig.shell" + source "drivers/video/Kconfig.sw_generator" source "drivers/video/Kconfig.mt9m114" diff --git a/drivers/video/Kconfig.shell b/drivers/video/Kconfig.shell new file mode 100644 index 000000000000..0da2fde7d5c1 --- /dev/null +++ b/drivers/video/Kconfig.shell @@ -0,0 +1,20 @@ +# Copyright The Zephyr Project Contributors +# SPDX-License-Identifier: Apache-2.0 + +config VIDEO_SHELL + bool "Video shell" + depends on SHELL + help + This shell provides control and query commands for video drivers. + +if VIDEO_SHELL + +config VIDEO_SHELL_CTRL_NAME_SIZE + int "Maximum size for the control string identifier" + default 40 + help + Video controls have a human-friendly name that is converted into a string identifier. + This Kconfig controls the maximum size of the string identifier after which the name + would be truncated. The default value is enough for all standard control names. + +endif diff --git a/drivers/video/video_shell.c b/drivers/video/video_shell.c new file mode 100644 index 000000000000..fe1a6402eef1 --- /dev/null +++ b/drivers/video/video_shell.c @@ -0,0 +1,1078 @@ +/* + * Copyright The Zephyr Project Contributors + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include + +#include +#include +#include +#include +#include + +#include "video_ctrls.h" + +#define VIDEO_FRMIVAL_FPS(frmival) DIV_ROUND_CLOSEST((frmival)->denominator, (frmival)->numerator) +#define VIDEO_FRMIVAL_MSEC(frmival) (MSEC_PER_SEC * (frmival)->numerator / (frmival)->denominator) + +/* Helper to allow completion of sub-command with content that depends on previous selection */ +#define VIDEO_SHELL_COMPLETE_DEFINE(n, name) \ + static void complete_##name##n(size_t idx, struct shell_static_entry *entry) \ + { \ + complete_##name(idx, entry, n); \ + } \ + SHELL_DYNAMIC_CMD_CREATE(dsub_##name##n, complete_##name##n) + +/* Current video device, used for tab completion */ +static const struct device *video_shell_dev; + +/* Current video control, used for tab completion */ +static struct video_ctrl_query video_shell_cq; + +/* Current buffer direction, used for tab completion */ +static enum video_buf_type video_shell_buf_type = VIDEO_BUF_TYPE_OUTPUT; + +/* A global variable holding the buffer set to entry->syntax for control identifiers */ +static char video_shell_ctrl_name[CONFIG_VIDEO_SHELL_CTRL_NAME_SIZE]; + +/* A global variable holding the buffer set to entry->syntax for control menu values */ +static char video_shell_menu_name[CONFIG_VIDEO_SHELL_CTRL_NAME_SIZE]; + +static bool device_is_video_and_ready(const struct device *dev) +{ + return device_is_ready(dev) && DEVICE_API_IS(video, dev); +} + +static int video_shell_check_device(const struct shell *sh, const struct device *dev) +{ + if (dev == NULL) { + shell_error(sh, "Could not find a video device with that name"); + return -ENODEV; + } + + if (!DEVICE_API_IS(video, dev)) { + shell_error(sh, "%s is not a video device", dev->name); + return -EINVAL; + } + + if (!device_is_ready(dev)) { + shell_error(sh, "Device %s not ready", dev->name); + return -ENODEV; + } + + return 0; +} + +static const struct device *video_shell_get_dev_by_num(int num) +{ + const struct device *dev_list; + size_t n = z_device_get_all_static(&dev_list); + + for (size_t i = 0; i < n; i++) { + if (!DEVICE_API_IS(video, (&dev_list[i]))) { + continue; + } + if (num == 0) { + return &dev_list[i]; + } + num--; + } + + return NULL; +} + +static int video_shell_parse_in_out(const struct shell *sh, char const *arg_in_out, + enum video_buf_type *type) +{ + if (strcmp(arg_in_out, "in") == 0) { + *type = VIDEO_BUF_TYPE_INPUT; + } else if (strcmp(arg_in_out, "out") == 0) { + *type = VIDEO_BUF_TYPE_OUTPUT; + } else { + shell_error(sh, "Endpoint direction must be 'in' or 'out', not '%s'", arg_in_out); + return -EINVAL; + } + + return 0; +} + +static int video_shell_parse_on_off(const struct shell *sh, char const *arg_on_off, int32_t *value) +{ + if (strcmp(arg_on_off, "on") == 0 || strcmp(arg_on_off, "1") == 0) { + *value = true; + } else if (strcmp(arg_on_off, "off") == 0 || strcmp(arg_on_off, "0") == 0) { + *value = false; + } else { + shell_error(sh, "Endpoint direction must be 'on' or 'off', not '%s'", arg_on_off); + return -EINVAL; + } + + return 0; +} + +static int cmd_video_start(const struct shell *sh, size_t argc, char **argv) +{ + const struct device *dev; + char *arg_device = argv[1]; + enum video_buf_type type; + int ret; + + dev = device_get_binding(arg_device); + ret = video_shell_check_device(sh, dev); + if (ret < 0) { + return ret; + } + + /* Only starting of output is supported for now */ + type = VIDEO_BUF_TYPE_OUTPUT; + + ret = video_stream_start(dev, type); + if (ret < 0) { + shell_error(sh, "Failed to start %s", dev->name); + return ret; + } + + shell_print(sh, "Started %s video stream", dev->name); + return 0; +} + +static int cmd_video_stop(const struct shell *sh, size_t argc, char **argv) +{ + const struct device *dev; + char *arg_device = argv[1]; + enum video_buf_type type; + int ret; + + dev = device_get_binding(arg_device); + ret = video_shell_check_device(sh, dev); + if (ret < 0) { + return ret; + } + + /* Only starting of output is supported for now */ + type = VIDEO_BUF_TYPE_OUTPUT; + + ret = video_stream_stop(dev, type); + if (ret < 0) { + shell_error(sh, "Failed to stop %s", dev->name); + return ret; + } + + shell_print(sh, "Stopped %s video stream", dev->name); + return 0; +} + +static void video_shell_print_buffer(const struct shell *sh, struct video_buffer *vbuf, + struct video_format *fmt, int i, uint32_t num_buffer, + uint32_t frmrate_fps, uint32_t frmival_msec) +{ + uint32_t line_offset = vbuf->line_offset; + uint32_t byte_offset = line_offset * fmt->pitch; + uint32_t bytes_in_buf = vbuf->bytesused; + uint32_t lines_in_buf = vbuf->bytesused / fmt->pitch; + + shell_print(sh, "Buffer %u/%u at %u ms, Bytes %u-%u/%u, Lines %u-%u/%u, Rate %u FPS %u ms", + /* Buffer */ i + 1, num_buffer, vbuf->timestamp, + /* Bytes */ byte_offset, byte_offset + bytes_in_buf, fmt->height * fmt->pitch, + /* Lines */ line_offset, line_offset + lines_in_buf, fmt->height, + /* Rate */ frmrate_fps, frmival_msec); +} + +static int cmd_video_capture(const struct shell *sh, size_t argc, char **argv) +{ + const struct device *dev; + struct video_format fmt = {.type = VIDEO_BUF_TYPE_OUTPUT}; + struct video_buffer *buffers[CONFIG_VIDEO_BUFFER_POOL_NUM_MAX] = {NULL}; + struct video_buffer vbuf0 = {.type = VIDEO_BUF_TYPE_OUTPUT}; + struct video_buffer *vbuf = &vbuf0; + char *arg_device = argv[1]; + char *arg_nbufs = argv[2]; + uint32_t first_uptime; + uint32_t prev_uptime; + uint32_t this_uptime; + uint32_t frmival_msec; + uint32_t frmrate_fps; + size_t buf_size; + unsigned long num_buffers; + int ret; + + dev = device_get_binding(arg_device); + ret = video_shell_check_device(sh, dev); + if (ret < 0) { + return ret; + } + + ret = video_get_format(dev, &fmt); + if (ret < 0) { + shell_error(sh, "Failed to get the current format interval"); + return ret; + } + + num_buffers = strtoull(arg_nbufs, &arg_nbufs, 10); + if (*arg_nbufs != '\0') { + shell_error(sh, "Invalid integer '%s' for this type", arg_nbufs); + return -EINVAL; + } + + buf_size = MIN(fmt.pitch * fmt.height, CONFIG_VIDEO_BUFFER_POOL_SZ_MAX); + + shell_print(sh, "Preparing %u buffers of %u bytes each", + CONFIG_VIDEO_BUFFER_POOL_NUM_MAX, buf_size); + + for (int i = 0; i < ARRAY_SIZE(buffers); i++) { + buffers[i] = video_buffer_alloc(buf_size, K_NO_WAIT); + if (buffers[i] == NULL) { + shell_error(sh, "Failed to allocate buffer %u", i); + goto end; + } + + /* Only queueing of output buffers is supported for now */ + buffers[i]->type = VIDEO_BUF_TYPE_OUTPUT; + + ret = video_enqueue(dev, buffers[i]); + if (ret < 0) { + shell_error(sh, "Failed to enqueue buffer %u: %s", i, strerror(-ret)); + goto end; + } + } + + shell_print(sh, "Starting the capture of %lu buffers from %s", num_buffers, dev->name); + + ret = video_stream_start(dev, VIDEO_BUF_TYPE_OUTPUT); + if (ret < 0) { + shell_error(sh, "Failed to start %s", dev->name); + goto end; + } + + first_uptime = prev_uptime = this_uptime = k_uptime_get_32(); + + for (unsigned long i = 0; i < num_buffers;) { + ret = video_dequeue(dev, &vbuf, K_FOREVER); + if (ret < 0) { + shell_error(sh, "Failed to dequeue this buffer: %s", strerror(-ret)); + goto end; + } + + this_uptime = k_uptime_get_32(); + frmival_msec = this_uptime - prev_uptime; + frmrate_fps = (frmival_msec == 0) ? (UINT32_MAX) : (MSEC_PER_SEC / frmival_msec); + prev_uptime = this_uptime; + + video_shell_print_buffer(sh, vbuf, &fmt, i, num_buffers, frmrate_fps, frmival_msec); + + /* Only increment the frame counter on the beginning of a new frame */ + i += (vbuf->line_offset == 0); + + ret = video_enqueue(dev, vbuf); + if (ret < 0) { + shell_error(sh, "Failed to enqueue this buffer: %s", strerror(-ret)); + goto end; + } + } + + frmival_msec = this_uptime - first_uptime; + frmrate_fps = + (frmival_msec == 0) ? (UINT32_MAX) : (num_buffers * MSEC_PER_SEC / frmival_msec); + + shell_print(sh, "Capture of %lu buffers in %u ms in total, %u FPS on average, stopping %s", + num_buffers, frmival_msec, frmrate_fps, dev->name); + +end: + video_stream_stop(dev, VIDEO_BUF_TYPE_OUTPUT); + + if (vbuf != NULL) { + video_buffer_release(vbuf); + } + + while (video_dequeue(dev, &vbuf, K_NO_WAIT) == 0) { + video_buffer_release(vbuf); + } + + return ret; +} + +static void complete_video_dev(size_t idx, struct shell_static_entry *entry) +{ + const struct device *dev; + + entry->handler = NULL; + entry->help = NULL; + entry->subcmd = NULL; + entry->syntax = NULL; + + dev = shell_device_filter(idx, device_is_video_and_ready); + if (dev == NULL) { + return; + } + + entry->syntax = dev->name; +} +SHELL_DYNAMIC_CMD_CREATE(dsub_video_dev, complete_video_dev); + +/* Video frame interval handling */ + +static int video_shell_print_frmival(const struct shell *sh, const struct device *dev, + uint32_t pixfmt, uint32_t width, uint32_t height) +{ + struct video_format fmt = {.pixelformat = pixfmt, .width = width, .height = height}; + struct video_frmival_enum fie = {.format = &fmt}; + + for (fie.index = 0; video_enum_frmival(dev, &fie) == 0; fie.index++) { + + switch (fie.type) { + case VIDEO_FRMIVAL_TYPE_DISCRETE: + shell_print(sh, "\t\t\tInterval: " + "Discrete %u ms (%u FPS)", + VIDEO_FRMIVAL_MSEC(&fie.discrete), + VIDEO_FRMIVAL_FPS(&fie.discrete)); + break; + case VIDEO_FRMIVAL_TYPE_STEPWISE: + shell_print(sh, "\t\t\tInterval: " + "Stepwise: %u ms - %u ms with step %u ms (%u - %u FPS)", + VIDEO_FRMIVAL_MSEC(&fie.stepwise.min), + VIDEO_FRMIVAL_MSEC(&fie.stepwise.max), + VIDEO_FRMIVAL_MSEC(&fie.stepwise.step), + VIDEO_FRMIVAL_FPS(&fie.stepwise.max), + VIDEO_FRMIVAL_FPS(&fie.stepwise.min)); + break; + default: + shell_error(sh, "Invalid type 0x%x", fie.type); + break; + } + } + + if (fie.index == 0) { + shell_warn(sh, "No frame interval supported for this format for output endpoint"); + } + + return 0; +} + +static int video_shell_set_frmival(const struct shell *sh, const struct device *dev, + size_t argc, char **argv) +{ + struct video_frmival frmival = {0}; + char *arg_rate = argv[0]; + char *end_rate; + unsigned long val; + int ret; + + val = strtoul(arg_rate, &end_rate, 10); + if (strcmp(end_rate, "fps") == 0) { + frmival.numerator = 1; + frmival.denominator = val; + } else if (strcmp(end_rate, "ms") == 0) { + frmival.numerator = val; + frmival.denominator = MSEC_PER_SEC; + } else if (strcmp(end_rate, "us") == 0) { + frmival.numerator = val; + frmival.denominator = USEC_PER_SEC; + } else { + shell_error(sh, "Expected ms, us or fps, not '%s'", arg_rate); + return -EINVAL; + } + + ret = video_set_frmival(dev, &frmival); + if (ret < 0) { + shell_error(sh, "Failed to set frame interval"); + return ret; + } + + shell_print(sh, "New frame interval: %u ms (%u FPS)", + VIDEO_FRMIVAL_MSEC(&frmival), VIDEO_FRMIVAL_FPS(&frmival)); + + return 0; +} + +static int cmd_video_frmival(const struct shell *sh, size_t argc, char **argv) +{ + const struct device *dev; + struct video_format fmt = {.type = video_shell_buf_type}; + struct video_frmival frmival = {0}; + char *arg_device = argv[1]; + int ret; + + dev = device_get_binding(arg_device); + ret = video_shell_check_device(sh, dev); + if (ret < 0) { + return ret; + } + + ret = video_get_frmival(dev, &frmival); + if (ret < 0) { + shell_error(sh, "Failed to get the current frame interval"); + return ret; + } + + ret = video_get_format(dev, &fmt); + if (ret < 0) { + shell_error(sh, "Failed to get the current format interval"); + return ret; + } + + shell_print(sh, "Current frame interval: %u ms (%u FPS)", + VIDEO_FRMIVAL_MSEC(&frmival), VIDEO_FRMIVAL_FPS(&frmival)); + + switch (argc) { + case 2: + return video_shell_print_frmival(sh, dev, fmt.pixelformat, fmt.width, + fmt.height); + case 3: + return video_shell_set_frmival(sh, dev, argc - 2, &argv[2]); + default: + shell_error(sh, "Wrong parameter count"); + return -EINVAL; + } +} + +static void complete_video_frmival_dev(size_t idx, struct shell_static_entry *entry) +{ + const struct device *dev; + + entry->handler = NULL; + entry->help = NULL; + entry->subcmd = NULL; + entry->syntax = NULL; + + dev = shell_device_filter(idx, device_is_video_and_ready); + if (dev == NULL) { + return; + } + + entry->syntax = dev->name; +} +SHELL_DYNAMIC_CMD_CREATE(dsub_video_frmival_dev, complete_video_frmival_dev); + +/* Video format handling */ + +static int video_shell_print_format_cap(const struct shell *sh, const struct device *dev, + enum video_buf_type type, + const struct video_format_cap *cap, size_t i) +{ + size_t bpp = video_bits_per_pixel(cap->pixelformat); + size_t size_max = cap->width_max * cap->height_max * bpp / BITS_PER_BYTE; + size_t size_min = cap->width_min * cap->height_min * bpp / BITS_PER_BYTE; + int ret; + + shell_print(sh, "\t[%u]: '%s' (%u bits per pixel)", + i, VIDEO_FOURCC_TO_STR(cap->pixelformat), bpp); + + if (cap->width_min == cap->width_max && cap->height_min == cap->height_max) { + shell_print(sh, "\t\tSize: " + "Discrete: %ux%u (%u bytes per frame)", + cap->width_max, cap->height_max, size_min); + + ret = video_shell_print_frmival(sh, dev, cap->pixelformat, cap->width_min, + cap->height_min); + if (ret < 0) { + return ret; + } + } else { + shell_print(sh, "\t\tSize: " + "Stepwise: %ux%u - %ux%u with step %ux%u (%u - %u bytes per frame)", + cap->width_min, cap->height_min, cap->width_max, cap->height_max, + cap->width_step, cap->height_step, size_min, size_max); + + ret = video_shell_print_frmival(sh, dev, cap->pixelformat, cap->width_min, + cap->height_min); + if (ret < 0) { + return ret; + } + + ret = video_shell_print_frmival(sh, dev, cap->pixelformat, cap->width_max, + cap->height_max); + if (ret < 0) { + return ret; + } + } + + return 0; +} + +static int video_shell_print_caps(const struct shell *sh, const struct device *dev, + struct video_caps *caps) +{ + int ret; + + shell_print(sh, "min vbuf count: %u", caps->min_vbuf_count); + shell_print(sh, "min line count: %u", caps->min_line_count); + shell_print(sh, "max line count: %u", caps->max_line_count); + + for (size_t i = 0; caps->format_caps[i].pixelformat != 0; i++) { + ret = video_shell_print_format_cap(sh, dev, caps->type, &caps->format_caps[i], i); + if (ret < 0) { + return ret; + } + } + + return 0; +} + +static int video_shell_set_format(const struct shell *sh, const struct device *dev, + enum video_buf_type type, size_t argc, char **argv) +{ + struct video_format fmt = {.type = video_shell_buf_type}; + char *arg_fourcc = argv[0]; + char *arg_size = argv[1]; + char *end_size; + int ret; + + if (strlen(arg_fourcc) != 4) { + shell_error(sh, "Invalid four character code: '%s'", arg_fourcc); + return -EINVAL; + } + + fmt.pixelformat = VIDEO_FOURCC_FROM_STR(arg_fourcc); + + fmt.width = strtoul(arg_size, &end_size, 10); + if (*end_size != 'x' || fmt.width == 0) { + shell_error(sh, "Invalid width in x parameter %s", arg_size); + return -EINVAL; + } + end_size++; + + fmt.height = strtoul(end_size, &end_size, 10); + if (*end_size != '\0' || fmt.height == 0) { + shell_error(sh, "Invalid height in x parameter %s (%s)", + arg_size, end_size); + return -EINVAL; + } + + fmt.pitch = fmt.width * video_bits_per_pixel(fmt.pixelformat) / BITS_PER_BYTE; + + ret = video_set_format(dev, &fmt); + if (ret < 0) { + shell_error(sh, "Failed to set the format to '%s' %ux%u on output endpoint", + VIDEO_FOURCC_TO_STR(fmt.pixelformat), fmt.width, fmt.height); + return ret; + } + + shell_print(sh, "New format: '%s' %ux%u (%u bytes per frame)", + VIDEO_FOURCC_TO_STR(fmt.pixelformat), fmt.width, fmt.height, + fmt.pitch * fmt.height); + + return 0; +} + +static int cmd_video_format(const struct shell *sh, size_t argc, char **argv) +{ + const struct device *dev; + struct video_caps caps = {0}; + struct video_format fmt = {0}; + enum video_buf_type type; + char *arg_device = argv[1]; + char *arg_in_out = argv[2]; + int ret; + + dev = device_get_binding(arg_device); + ret = video_shell_check_device(sh, dev); + if (ret < 0) { + return ret; + } + + ret = video_shell_parse_in_out(sh, arg_in_out, &type); + if (ret < 0) { + return -ret; + } + fmt.type = caps.type = type; + + ret = video_get_caps(dev, &caps); + if (ret < 0) { + shell_error(sh, "Failed to query %s capabilities", dev->name); + return -ENODEV; + } + + ret = video_get_format(dev, &fmt); + if (ret < 0) { + shell_error(sh, "Failed to query %s capabilities", dev->name); + return -ENODEV; + } + + shell_print(sh, "Current format: '%s' %ux%u (%u bytes per frame)", + VIDEO_FOURCC_TO_STR(fmt.pixelformat), fmt.width, fmt.height, + fmt.pitch * fmt.height); + + switch (argc) { + case 3: + return video_shell_print_caps(sh, dev, &caps); + case 5: + return video_shell_set_format(sh, dev, type, argc - 3, &argv[3]); + default: + shell_error(sh, "Wrong parameter count"); + return -EINVAL; + } +} + +/* Video control handling */ + +static void video_shell_convert_ctrl_name(char const *src, char *dst, size_t dst_size) +{ + size_t o = 0; + bool add_underscore = false; + + for (size_t i = 0; src[i] != '\0'; i++) { + if (o >= dst_size) { + break; + } + + if (isalnum(src[i])) { + dst[o] = tolower(src[i]); + o++; + add_underscore = true; + } else if (add_underscore) { + dst[o] = '_'; + o++; + add_underscore = false; + } else { + /* skip */ + } + } + + do { + dst[o] = '\0'; + } while (o-- > 0 && !isalnum(dst[o])); +} + +static int video_shell_get_ctrl_by_name(const struct device *dev, struct video_ctrl_query *cq, + char const *name) +{ + char ctrl_name[CONFIG_VIDEO_SHELL_CTRL_NAME_SIZE]; + int ret; + + cq->id = 0; + for (size_t i = 0;; i++) { + cq->id |= VIDEO_CTRL_FLAG_NEXT_CTRL; + + ret = video_query_ctrl(dev, cq); + if (ret < 0) { + return -ENOENT; + } + + video_shell_convert_ctrl_name(name, ctrl_name, sizeof(ctrl_name)); + if (strcmp(ctrl_name, name) == 0) { + break; + } + } + + return 0; +} + +static int video_shell_get_ctrl_by_num(const struct device *dev, struct video_ctrl_query *cq, + int num) +{ + int ret; + + cq->id = 0; + for (size_t i = 0; i <= num; i++) { + cq->id |= VIDEO_CTRL_FLAG_NEXT_CTRL; + + ret = video_query_ctrl(dev, cq); + if (ret < 0) { + return -ENOENT; + } + } + + return 0; +} + +static int video_shell_set_ctrl(const struct shell *sh, const struct device *dev, size_t argc, + char **argv) +{ + struct video_ctrl_query cq = {0}; + struct video_control ctrl; + char menu_name[CONFIG_VIDEO_SHELL_CTRL_NAME_SIZE]; + char *arg_ctrl = argv[0]; + char *arg_value = argv[1]; + char *end_value; + size_t i; + int ret; + + ret = video_shell_get_ctrl_by_name(dev, &cq, arg_ctrl); + if (ret < 0) { + shell_error(sh, "Video control '%s' not found for this device", arg_ctrl); + return ret; + } + + ctrl.id = cq.id; + + switch (cq.type) { + case VIDEO_CTRL_TYPE_MENU: + for (i = 0; cq.menu[i] != NULL; i++) { + video_shell_convert_ctrl_name(cq.menu[i], menu_name, sizeof(menu_name)); + if (strcmp(menu_name, arg_value) == 0) { + ctrl.val64 = i; + } + } + if (cq.menu[i] != NULL) { + shell_error(sh, "Video control value '%s' is not present in the menu", + arg_value); + return -EINVAL; + } + break; + case VIDEO_CTRL_TYPE_BOOLEAN: + ret = video_shell_parse_on_off(sh, arg_value, &ctrl.val); + if (ret < 0) { + return ret; + } + break; + case VIDEO_CTRL_TYPE_INTEGER: + ctrl.val = strtol(arg_value, &end_value, 10); + if (*end_value != '\0') { + shell_error(sh, "Invalid integer '%s' for this type", arg_value); + return -EINVAL; + } + break; + case VIDEO_CTRL_TYPE_INTEGER64: + ctrl.val64 = strtoll(arg_value, &arg_value, 10); + if (*end_value != '\0') { + shell_error(sh, "Invalid integer '%s' for this type", arg_value); + return -EINVAL; + } + break; + default: + CODE_UNREACHABLE; + } + + shell_print(sh, "Setting control '%s' (0x%08x) to value '%s' (%lld)", + arg_ctrl, ctrl.id, arg_value, + (long long)(cq.type == VIDEO_CTRL_TYPE_INTEGER64 ? ctrl.val64 : ctrl.val)); + + ret = video_set_ctrl(dev, &ctrl); + if (ret < 0) { + shell_error(sh, "Failed to update control 0x%08x", ctrl.id); + return ret; + } + + return 0; +} + +static int video_shell_print_ctrls(const struct shell *sh, const struct device *dev) +{ + struct video_ctrl_query cq = {.id = VIDEO_CTRL_FLAG_NEXT_CTRL}; + char ctrl_name[CONFIG_VIDEO_SHELL_CTRL_NAME_SIZE]; + + while (video_query_ctrl(dev, &cq) == 0) { + /* Convert from human-friendly form to machine-friendly */ + video_shell_convert_ctrl_name(cq.name, ctrl_name, sizeof(ctrl_name)); + cq.name = ctrl_name; + + video_print_ctrl(dev, &cq); + cq.id |= VIDEO_CTRL_FLAG_NEXT_CTRL; + } + + return 0; +} + +static int cmd_video_ctrl(const struct shell *sh, size_t argc, char **argv) +{ + const struct device *dev; + char *arg_device = argv[1]; + int ret; + + dev = device_get_binding(arg_device); + ret = video_shell_check_device(sh, dev); + if (ret < 0) { + return ret; + } + + switch (argc) { + case 2: + return video_shell_print_ctrls(sh, dev); + case 4: + return video_shell_set_ctrl(sh, dev, argc - 2, &argv[2]); + default: + shell_error(sh, "Wrong parameter count"); + return -EINVAL; + } + + return 0; +} + +static void complete_video_ctrl_boolean(size_t idx, struct shell_static_entry *entry) +{ + static const char *const syntax[] = {"on", "off"}; + + entry->handler = NULL; + entry->help = NULL; + entry->subcmd = NULL; + entry->syntax = NULL; + + if (idx >= ARRAY_SIZE(syntax)) { + return; + } + + entry->syntax = syntax[idx]; +} +SHELL_DYNAMIC_CMD_CREATE(dsub_video_ctrl_boolean, complete_video_ctrl_boolean); + +static void complete_video_ctrl_menu_name(size_t idx, struct shell_static_entry *entry, int ctrln) +{ + const struct device *dev = video_shell_dev; + struct video_ctrl_query cq = {0}; + int ret; + + entry->handler = NULL; + entry->help = NULL; + entry->subcmd = NULL; + entry->syntax = NULL; + + dev = video_shell_dev; + if (!device_is_ready(dev)) { + return; + } + + /* Check which control name was selected */ + + ret = video_shell_get_ctrl_by_num(dev, &cq, ctrln); + if (ret < 0) { + return; + } + + /* Then complete the menu value */ + + for (size_t i = 0; cq.menu[i] != NULL; i++) { + if (i == idx) { + video_shell_convert_ctrl_name(cq.menu[i], video_shell_menu_name, + sizeof(video_shell_menu_name)); + entry->syntax = video_shell_menu_name; + break; + } + } + +} +LISTIFY(10, VIDEO_SHELL_COMPLETE_DEFINE, (;), video_ctrl_menu_name); + +static const union shell_cmd_entry *dsub_video_ctrl_menu_name[] = { + &dsub_video_ctrl_menu_name0, &dsub_video_ctrl_menu_name1, + &dsub_video_ctrl_menu_name2, &dsub_video_ctrl_menu_name3, + &dsub_video_ctrl_menu_name4, &dsub_video_ctrl_menu_name5, + &dsub_video_ctrl_menu_name6, &dsub_video_ctrl_menu_name7, + &dsub_video_ctrl_menu_name8, &dsub_video_ctrl_menu_name9, +}; + +static void complete_video_ctrl_name_dev(size_t idx, struct shell_static_entry *entry, int devn) +{ + const struct device *dev; + struct video_ctrl_query *cq = &video_shell_cq; + int ret; + + + entry->handler = NULL; + entry->help = NULL; + entry->subcmd = NULL; + entry->syntax = NULL; + + /* Check which device was selected */ + + dev = video_shell_dev = video_shell_get_dev_by_num(devn); + if (!device_is_ready(dev)) { + return; + } + + /* Then complete the control name */ + + ret = video_shell_get_ctrl_by_num(dev, &video_shell_cq, idx); + if (ret < 0) { + return; + } + + video_shell_convert_ctrl_name(video_shell_cq.name, video_shell_ctrl_name, + sizeof(video_shell_ctrl_name)); + entry->syntax = video_shell_ctrl_name; + + switch (cq->type) { + case VIDEO_CTRL_TYPE_BOOLEAN: + entry->subcmd = &dsub_video_ctrl_boolean; + break; + case VIDEO_CTRL_TYPE_MENU: + if (idx >= ARRAY_SIZE(dsub_video_ctrl_menu_name)) { + return; + } + entry->subcmd = dsub_video_ctrl_menu_name[idx]; + break; + default: + entry->subcmd = NULL; + break; + } +} +LISTIFY(10, VIDEO_SHELL_COMPLETE_DEFINE, (;), video_ctrl_name_dev); + +static const union shell_cmd_entry *dsub_video_ctrl_name_dev[] = { + &dsub_video_ctrl_name_dev0, &dsub_video_ctrl_name_dev1, + &dsub_video_ctrl_name_dev2, &dsub_video_ctrl_name_dev3, + &dsub_video_ctrl_name_dev4, &dsub_video_ctrl_name_dev5, + &dsub_video_ctrl_name_dev6, &dsub_video_ctrl_name_dev7, + &dsub_video_ctrl_name_dev8, &dsub_video_ctrl_name_dev9, +}; + +static void complete_video_ctrl_dev(size_t idx, struct shell_static_entry *entry) +{ + const struct device *dev; + + entry->syntax = NULL; + entry->handler = NULL; + entry->help = NULL; + entry->subcmd = NULL; + + dev = video_shell_get_dev_by_num(idx); + if (!device_is_ready(dev) || idx >= ARRAY_SIZE(dsub_video_ctrl_name_dev)) { + return; + } + + entry->syntax = dev->name; + entry->subcmd = dsub_video_ctrl_name_dev[idx]; +} +SHELL_DYNAMIC_CMD_CREATE(dsub_video_ctrl_dev, complete_video_ctrl_dev); + +static void complete_video_format_name(size_t idx, struct shell_static_entry *entry, + enum video_buf_type type) +{ + const struct device *dev = video_shell_dev; + static char fourcc[5] = {0}; + struct video_caps caps = {.type = video_shell_buf_type}; + uint32_t prev_pixfmt = 0; + uint32_t pixfmt = 0; + int ret; + + entry->syntax = NULL; + entry->handler = NULL; + entry->help = NULL; + entry->subcmd = NULL; + + /* Fill which buffer type was given in the partial input */ + + video_shell_buf_type = type; + + /* Then complete the format name */ + + ret = video_get_caps(dev, &caps); + if (ret < 0) { + return; + } + + for (size_t i = 0; caps.format_caps[i].pixelformat != 0; i++) { + if (idx == 0) { + pixfmt = caps.format_caps[i].pixelformat; + break; + } + + if (prev_pixfmt != caps.format_caps[i].pixelformat) { + idx--; + } + prev_pixfmt = caps.format_caps[i].pixelformat; + } + if (pixfmt == 0) { + return; + } + + memcpy(fourcc, VIDEO_FOURCC_TO_STR(pixfmt), sizeof(fourcc)); + entry->syntax = fourcc; +} +static void complete_video_format_in_name(size_t idx, struct shell_static_entry *entry) +{ + complete_video_format_name(idx, entry, VIDEO_BUF_TYPE_INPUT); +} +static void complete_video_format_out_name(size_t idx, struct shell_static_entry *entry) +{ + complete_video_format_name(idx, entry, VIDEO_BUF_TYPE_OUTPUT); +} +SHELL_DYNAMIC_CMD_CREATE(dsub_video_format_in_name, complete_video_format_in_name); +SHELL_DYNAMIC_CMD_CREATE(dsub_video_format_out_name, complete_video_format_out_name); + +static void complete_video_format_dir_dev(size_t idx, struct shell_static_entry *entry, int devn) +{ + const struct device *dev; + + entry->syntax = NULL; + entry->handler = NULL; + entry->help = NULL; + entry->subcmd = NULL; + video_shell_dev = NULL; + + /* Check which device was selected */ + + dev = video_shell_dev = video_shell_get_dev_by_num(devn); + if (!device_is_ready(dev)) { + return; + } + + /* Then complete the endpoint direction */ + + switch (idx) { + case 0: + entry->subcmd = &dsub_video_format_in_name; + entry->syntax = "in"; + break; + case 1: + entry->subcmd = &dsub_video_format_out_name; + entry->syntax = "out"; + break; + default: + entry->syntax = NULL; + break; + } +} +LISTIFY(10, VIDEO_SHELL_COMPLETE_DEFINE, (;), video_format_dir_dev); + +static const union shell_cmd_entry *dsub_video_format_dir_dev[] = { + &dsub_video_format_dir_dev0, &dsub_video_format_dir_dev1, + &dsub_video_format_dir_dev2, &dsub_video_format_dir_dev3, + &dsub_video_format_dir_dev4, &dsub_video_format_dir_dev5, + &dsub_video_format_dir_dev6, &dsub_video_format_dir_dev7, + &dsub_video_format_dir_dev8, &dsub_video_format_dir_dev9, +}; + +static void complete_video_format_dev(size_t idx, struct shell_static_entry *entry) +{ + const struct device *dev; + + entry->syntax = NULL; + entry->handler = NULL; + entry->help = NULL; + entry->subcmd = NULL; + + dev = video_shell_get_dev_by_num(idx); + if (!device_is_ready(dev) || idx >= ARRAY_SIZE(dsub_video_format_dir_dev)) { + return; + } + + entry->syntax = dev->name; + entry->subcmd = dsub_video_format_dir_dev[idx]; +} +SHELL_DYNAMIC_CMD_CREATE(dsub_video_format_dev, complete_video_format_dev); + +/* Video shell commands declaration */ + +SHELL_STATIC_SUBCMD_SET_CREATE(sub_video_cmds, + SHELL_CMD_ARG(start, &dsub_video_dev, + "Start a video device and its sources\n" + "Usage: video start ", + cmd_video_start, 2, 0), + SHELL_CMD_ARG(stop, &dsub_video_dev, + "Stop a video device and its sources\n" + "Usage: video stop ", + cmd_video_stop, 2, 0), + SHELL_CMD_ARG(capture, &dsub_video_dev, + "Capture a given number of buffers from a device\n" + "Usage: video capture ", + cmd_video_capture, 3, 0), + SHELL_CMD_ARG(format, &dsub_video_format_dev, + "Query or set the video format of a device\n" + "Usage: video format [ x]", + cmd_video_format, 3, 2), + SHELL_CMD_ARG(frmival, &dsub_video_frmival_dev, + "Query or set the video frame rate/interval of a device\n" + "Usage: video frmival [fps|ms|us]", + cmd_video_frmival, 2, 1), + SHELL_CMD_ARG(ctrl, &dsub_video_ctrl_dev, + "Query or set video controls of a device\n" + "Usage: video ctrl [ ]", + cmd_video_ctrl, 2, 2), + SHELL_SUBCMD_SET_END +); + +SHELL_CMD_REGISTER(video, &sub_video_cmds, "Video driver commands", NULL); diff --git a/samples/drivers/video/capture/README.rst b/samples/drivers/video/capture/README.rst index 3bbf9bc1213c..8216fdd46d9f 100644 --- a/samples/drivers/video/capture/README.rst +++ b/samples/drivers/video/capture/README.rst @@ -83,10 +83,29 @@ using the :ref:`dvp_20pin_ov7670` and :ref:`lcd_par_s035` connected to the board :goals: build :compact: +For :ref:`native_sim`, build this sample application with the following commands: + +.. zephyr-app-commands:: + :zephyr-app: samples/drivers/video/capture + :board: native_sim + :goals: build + :compact: + For testing purpose without the need of any real video capture and/or display hardwares, a video software pattern generator is supported by the above build commands without specifying the shields. +For controlling the camera device using shell commands instead of continuously capturing the data, +append ``-DCONFIG_VIDEO_SHELL=y`` to the build command: + +.. zephyr-app-commands:: + :zephyr-app: samples/drivers/video/capture + :board: mimxrt1064_evk + :shield: dvp_fpc24_mt9m114,rk043fn66hs_ctg + :gen-args: -DCONFIG_VIDEO_SHELL=y + :goals: build + :compact: + Sample Output ============= @@ -118,6 +137,28 @@ Sample Output +If using the shell, the capture would not start, and instead it is possible to access the shell + +.. code-block:: console + + uart:~$ video + video - Video driver commands + Subcommands: + start : Start a video device and its sources + Usage: video start + stop : Stop a video device and its sources + Usage: video stop + capture : Capture a given number of frames from a device + Usage: video capture + format : Query or set the video format of a device + Usage: video format [ x] + frmival : Query or set the video frame rate/interval of a device + Usage: video frmival [fps|ms|us] + ctrl : Query or set video controls of a device + Usage: video ctrl [ ] + uart:~$ + + References ********** diff --git a/samples/drivers/video/capture/sample.yaml b/samples/drivers/video/capture/sample.yaml index 318f3521f435..3c0338daeeb0 100644 --- a/samples/drivers/video/capture/sample.yaml +++ b/samples/drivers/video/capture/sample.yaml @@ -36,3 +36,21 @@ tests: integration_platforms: - mimxrt1064_evk - mimxrt1170_evk/mimxrt1176/cm7 + sample.video.capture.shell: + tags: + - video + - samples + - shell + extra_configs: + - CONFIG_VIDEO_SHELL=y + - CONFIG_FPU=y + - CONFIG_DISPLAY=n + harness: console + harness_config: + type: one_line + regex: + - "Letting the user control the device with the video shell" + platform_allow: + - native_sim/native/64 + integration_platforms: + - native_sim/native/64 diff --git a/samples/drivers/video/capture/src/main.c b/samples/drivers/video/capture/src/main.c index f5afea592542..5643afb29b2d 100644 --- a/samples/drivers/video/capture/src/main.c +++ b/samples/drivers/video/capture/src/main.c @@ -101,6 +101,12 @@ int main(void) int i = 0; int err; + /* When the video shell is enabled, do not run the capture loop */ + if (IS_ENABLED(CONFIG_VIDEO_SHELL)) { + LOG_INF("Letting the user control the device with the video shell"); + return 0; + } + #if DT_HAS_CHOSEN(zephyr_camera) const struct device *const video_dev = DEVICE_DT_GET(DT_CHOSEN(zephyr_camera)); @@ -202,10 +208,10 @@ int main(void) video_set_ctrl(video_dev, &ctrl); } -#ifdef CONFIG_TEST - ctrl.id = VIDEO_CID_TEST_PATTERN; - video_set_ctrl(video_dev, &ctrl); -#endif + if (IS_ENABLED(CONFIG_TEST)) { + ctrl.id = VIDEO_CID_TEST_PATTERN; + video_set_ctrl(video_dev, &ctrl); + } #if DT_HAS_CHOSEN(zephyr_display) const struct device *const display_dev = DEVICE_DT_GET(DT_CHOSEN(zephyr_display)); diff --git a/tests/drivers/build_all/video/prj.conf b/tests/drivers/build_all/video/prj.conf index b96d041d3b40..49d888fcab41 100644 --- a/tests/drivers/build_all/video/prj.conf +++ b/tests/drivers/build_all/video/prj.conf @@ -3,3 +3,4 @@ CONFIG_TEST_USERSPACE=y CONFIG_GPIO=y CONFIG_I2C=y CONFIG_VIDEO=y +CONFIG_VIDEO_SHELL=y