diff --git a/.gitmodules b/.gitmodules index d0b3f49..1c261cd 100644 --- a/.gitmodules +++ b/.gitmodules @@ -2,3 +2,6 @@ path = third_party/libdatachannel url = https://github.com/paullouisageneau/libdatachannel.git ignore = dirty +[submodule "third_party/magic_enum"] + path = third_party/magic_enum + url = https://github.com/Neargye/magic_enum.git diff --git a/RELEASE.md b/RELEASE.md index 7192ba8..6184ee5 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -1,5 +1,10 @@ # Release #{GIT_VERSION} +- http: extend `/status` with all options +- cmd: accept `--camera-options=AfMode=auto` and alike +- libcamera: expose all options with human readable settings +- v4l2: expose all options with human readable settings + ## Variants Download correct version for your platform: diff --git a/cmd/camera-streamer/opts.c b/cmd/camera-streamer/opts.c index 7bce5b6..b43dd4a 100644 --- a/cmd/camera-streamer/opts.c +++ b/cmd/camera-streamer/opts.c @@ -38,8 +38,8 @@ camera_options_t camera_options = { "video_bitrate=2000000" OPTION_VALUE_LIST_SEP "repeat_sequence_header=5000000" OPTION_VALUE_LIST_SEP "h264_i_frame_period=30" OPTION_VALUE_LIST_SEP - "h264_level=11" OPTION_VALUE_LIST_SEP - "h264_profile=4" OPTION_VALUE_LIST_SEP + "h264_level=4" OPTION_VALUE_LIST_SEP + "h264_profile=high" OPTION_VALUE_LIST_SEP "h264_minimum_qp_value=16" OPTION_VALUE_LIST_SEP "h264_maximum_qp_value=32" } diff --git a/cmd/camera-streamer/status.cc b/cmd/camera-streamer/status.cc index 36aecb9..3ddeb2e 100644 --- a/cmd/camera-streamer/status.cc +++ b/cmd/camera-streamer/status.cc @@ -2,12 +2,14 @@ extern "C" { #include "util/http/http.h" #include "util/opts/fourcc.h" +#include "util/opts/control.h" #include "device/buffer_list.h" #include "device/buffer_lock.h" #include "device/camera/camera.h" #include "output/rtsp/rtsp.h" #include "output/webrtc/webrtc.h" #include "output/output.h" +#include "version.h" extern camera_t *camera; extern http_server_options_t http_options; @@ -17,6 +19,7 @@ extern webrtc_options_t webrtc_options; }; #include +#include "third_party/magic_enum/include/magic_enum.hpp" static nlohmann::json serialize_buf_list(buffer_list_t *buf_list) { @@ -53,6 +56,48 @@ static nlohmann::json serialize_buf_lock(buffer_lock_t *buf_lock) return output; } +static const char *strip_prefix(const char *str, const char *prefix) +{ + if (strstr(str, prefix) == str) { + return str + strlen(prefix); + } + return str; +} + +static std::string std_device_option_normalize(std::string key) +{ + key.resize(device_option_normalize_name(key.data(), key.data())); + return key; +} + +static int device_options_callback(device_option_t *option, void *opaque) +{ + auto key = std_device_option_normalize(option->name); + nlohmann::json &device = *(nlohmann::json*)opaque; + nlohmann::json &node = option->flags.read_only ? + device["properties"][key] : + device["options"][key]; + + node["name"] = option->name; + node["type"] = strip_prefix( + std::string(magic_enum::enum_name(option->type)).c_str(), + "device_option_type_"); + + if (option->elems > 0) + node["elems"] = option->elems; + if (option->description[0]) + node["description"] = option->description; + if (option->value[0]) + node["value"] = option->value; + + for (int i = 0; i < option->menu_items; i++) { + char buf[64]; + sprintf(buf, "%d", option->menu[i].id); + node["menu"][buf] = option->menu[i].name; + } + return 0; +} + static nlohmann::json devices_status_json() { nlohmann::json devices; @@ -70,6 +115,8 @@ static nlohmann::json devices_status_json() for (int j = 0; j < device->n_capture_list; j++) { device_json["captures"][j] = serialize_buf_list(device->capture_lists[j]); } + + device_dump_options2(device, device_options_callback, &device_json); devices += device_json; } @@ -122,6 +169,9 @@ extern "C" void camera_status_json(http_worker_t *worker, FILE *stream) { nlohmann::json message; + message["version"] = GIT_VERSION; + message["revision"] = GIT_REVISION; + message["outputs"]["snapshot"] = serialize_buf_lock(&snapshot_lock); message["outputs"]["stream"] = serialize_buf_lock(&stream_lock); message["outputs"]["video"] = serialize_buf_lock(&video_lock); diff --git a/device/device.c b/device/device.c index 36826f9..30c1499 100644 --- a/device/device.c +++ b/device/device.c @@ -194,6 +194,14 @@ void device_dump_options(device_t *dev, FILE *stream) } } +int device_dump_options2(device_t *dev, device_option_fn fn, void *opaque) +{ + if (dev && dev->hw->device_dump_options) { + return dev->hw->device_dump_options2(dev, fn, opaque); + } + return -1; +} + int device_set_fps(device_t *dev, int desired_fps) { if (!dev) diff --git a/device/device.h b/device/device.h index e3b1c20..992871a 100644 --- a/device/device.h +++ b/device/device.h @@ -7,14 +7,18 @@ typedef struct buffer_s buffer_t; typedef struct buffer_list_s buffer_list_t; typedef struct buffer_format_s buffer_format_t; +typedef struct device_option_s device_option_t; typedef struct device_s device_t; struct pollfd; +typedef int device_option_fn(device_option_t *option, void *opaque); + typedef struct device_hw_s { int (*device_open)(device_t *dev); void (*device_close)(device_t *dev); int (*device_video_force_key)(device_t *dev); void (*device_dump_options)(device_t *dev, FILE *stream); + int (*device_dump_options2)(device_t *dev, device_option_fn fn, void *opaque); int (*device_set_fps)(device_t *dev, int desired_fps); int (*device_set_rotation)(device_t *dev, bool vflip, bool hflip); int (*device_set_option)(device_t *dev, const char *key, const char *value); @@ -55,6 +59,42 @@ typedef struct device_s { bool paused; } device_t; +typedef enum device_option_type_s { + device_option_type_u8, // comp-type + device_option_type_u16, // comp-type + device_option_type_u32, // comp-type + device_option_type_bool, + device_option_type_integer, + device_option_type_integer64, + device_option_type_float, + device_option_type_string +} device_option_type_t; + +#define MAX_DEVICE_OPTION_MENU 20 + +typedef struct device_option_menu_s { + int id; + char name[64]; +} device_option_menu_t; + +typedef struct device_option_s { + char name[64]; + unsigned int control_id; + + device_option_type_t type; + int elems; + struct { + bool read_only : 1; + bool invalid : 1; + } flags; + + device_option_menu_t menu[MAX_DEVICE_OPTION_MENU]; + int menu_items; + + char value[256]; + char description[256]; +} device_option_t; + device_t *device_open(const char *name, const char *path, device_hw_t *hw); void device_close(device_t *dev); @@ -68,6 +108,7 @@ int device_set_stream(device_t *dev, bool do_on); int device_video_force_key(device_t *dev); void device_dump_options(device_t *dev, FILE *stream); +int device_dump_options2(device_t *dev, device_option_fn fn, void *opaque); int device_set_fps(device_t *dev, int desired_fps); int device_set_rotation(device_t *dev, bool vflip, bool hflip); int device_set_option_string(device_t *dev, const char *option, const char *value); diff --git a/device/libcamera/buffer.cc b/device/libcamera/buffer.cc index de02b43..e01c200 100644 --- a/device/libcamera/buffer.cc +++ b/device/libcamera/buffer.cc @@ -85,7 +85,7 @@ int libcamera_buffer_enqueue(buffer_t *buf, const char *who) if (camera->queueRequest(buf->libcamera->request.get()) < 0) { LOG_ERROR(buf, "Can't queue buffer."); } - buf->buf_list->dev->libcamera->controls.clear(); + libcamera_device_apply_controls(buf->buf_list->dev); return 0; error: diff --git a/device/libcamera/buffer_list.cc b/device/libcamera/buffer_list.cc index ef88358..67765f5 100644 --- a/device/libcamera/buffer_list.cc +++ b/device/libcamera/buffer_list.cc @@ -170,7 +170,7 @@ int libcamera_buffer_list_set_stream(buffer_list_t *buf_list, bool do_on) LOG_ERROR(buf_list, "Failed to start camera."); } - buf_list->dev->libcamera->controls.clear(); + libcamera_device_apply_controls(buf_list->dev); } else { buf_list->dev->libcamera->camera->requestCompleted.disconnect( buf_list->libcamera, &buffer_list_libcamera_t::libcamera_buffer_list_dequeued); diff --git a/device/libcamera/device.cc b/device/libcamera/device.cc index 98d4d28..26b457f 100644 --- a/device/libcamera/device.cc +++ b/device/libcamera/device.cc @@ -1,63 +1,6 @@ #ifdef USE_LIBCAMERA #include "libcamera.hh" -std::string libcamera_device_option_normalize(std::string key) -{ - key.resize(device_option_normalize_name(key.data(), key.data())); - return key; -} - -libcamera::ControlInfoMap::Map libcamera_control_list(device_t *dev) -{ - libcamera::ControlInfoMap::Map controls_map; - for (auto const &control : dev->libcamera->camera->controls()) { - controls_map[control.first] = control.second; - } - return controls_map; -} - -void libcamera_device_dump_options(device_t *dev, FILE *stream) -{ - auto &properties = dev->libcamera->camera->properties(); - auto idMap = properties.idMap(); - - fprintf(stream, "%s Properties:\n", dev->name); - - for (auto const &control : properties) { - if (!control.first) - continue; - - auto control_id = control.first; - auto control_value = control.second; - std::string control_id_name = ""; - - if (auto control_id_info = idMap ? idMap->at(control_id) : NULL) { - control_id_name = control_id_info->name(); - } - - fprintf(stream, "- property: %s (%08x, type=%d): %s\n", - control_id_name.c_str(), control_id, control_value.type(), - control_value.toString().c_str()); - } - - fprintf(stream, "\n"); - fprintf(stream, "%s Options:\n", dev->name); - - for (auto const &control : libcamera_control_list(dev)) { - if (!control.first) - continue; - - auto control_id = control.first; - auto control_key = libcamera_device_option_normalize(control_id->name()); - auto control_info = control.second; - - fprintf(stream, "- available option: %s (%08x, type=%d): %s\n", - control_id->name().c_str(), control_id->id(), control_id->type(), - control_info.toString().c_str()); - } - fprintf(stream, "\n"); -} - void libcamera_print_cameras(device_t *dev) { if (dev->libcamera->camera_manager->cameras().size()) { @@ -153,99 +96,15 @@ int libcamera_device_set_rotation(device_t *dev, bool vflip, bool hflip) return 0; } -int libcamera_device_set_option(device_t *dev, const char *keyp, const char *value) +void libcamera_device_apply_controls(device_t *dev) { - auto key = libcamera_device_option_normalize(keyp); + auto &controls = dev->libcamera->controls; + auto &applied_controls = dev->libcamera->applied_controls; - for (auto const &control : libcamera_control_list(dev)) { - if (!control.first) - continue; - - auto control_id = control.first; - auto control_key = libcamera_device_option_normalize(control_id->name()); - if (key != control_key) - continue; - - libcamera::ControlValue control_value; - - switch (control_id->type()) { - case libcamera::ControlTypeNone: - break; - - case libcamera::ControlTypeBool: - control_value.set(atoi(value)); - break; - - case libcamera::ControlTypeByte: - control_value.set(atoi(value)); - break; - - case libcamera::ControlTypeInteger32: - control_value.set(atoi(value)); - break; - - case libcamera::ControlTypeInteger64: - control_value.set(atoi(value)); - break; - - case libcamera::ControlTypeFloat: - control_value.set(atof(value)); - break; - - case libcamera::ControlTypeRectangle: - static const char *RECTANGLE_PATTERNS[] = { - "(%d,%d)/%ux%u", - "%d,%d,%u,%u", - NULL - }; - - for (int i = 0; RECTANGLE_PATTERNS[i]; i++) { - libcamera::Rectangle rectangle; - - if (4 == sscanf(value, RECTANGLE_PATTERNS[i], - &rectangle.x, &rectangle.y, - &rectangle.width, &rectangle.height)) { - control_value.set(rectangle); - break; - } - } - break; - - case libcamera::ControlTypeSize: - static const char *SIZE_PATTERNS[] = { - "%ux%u", - "%u,%u", - NULL - }; - - for (int i = 0; SIZE_PATTERNS[i]; i++) { - libcamera::Size size; - - if (2 == sscanf(value, SIZE_PATTERNS[i], &size.width, &size.height)) { - control_value.set(size); - break; - } - } - break; - - case libcamera::ControlTypeString: - break; - } - - if (control_value.isNone()) { - LOG_ERROR(dev, "The `%s` type `%d` is not supported.", control_key.c_str(), control_id->type()); - } - - LOG_INFO(dev, "Configuring option %s (%08x, type=%d) = %s", - control_key.c_str(), control_id->id(), control_id->type(), - control_value.toString().c_str()); - dev->libcamera->controls.set(control_id->id(), control_value); - return 1; + for (auto &control : controls) { + applied_controls.set(control.first, control.second); } - return 0; - -error: - return -1; + controls.clear(); } #endif // USE_LIBCAMERA diff --git a/device/libcamera/libcamera.cc b/device/libcamera/libcamera.cc index 4123867..1f9494c 100644 --- a/device/libcamera/libcamera.cc +++ b/device/libcamera/libcamera.cc @@ -5,6 +5,7 @@ device_hw_t libcamera_device_hw = { .device_open = libcamera_device_open, .device_close = libcamera_device_close, .device_dump_options = libcamera_device_dump_options, + .device_dump_options2 = libcamera_device_dump_options2, .device_set_fps = libcamera_device_set_fps, .device_set_rotation = libcamera_device_set_rotation, .device_set_option = libcamera_device_set_option, diff --git a/device/libcamera/libcamera.hh b/device/libcamera/libcamera.hh index 99685e0..723c7ce 100644 --- a/device/libcamera/libcamera.hh +++ b/device/libcamera/libcamera.hh @@ -42,6 +42,7 @@ typedef struct device_libcamera_s { std::shared_ptr configuration; std::shared_ptr allocator; libcamera::ControlList controls; + libcamera::ControlList applied_controls; bool vflip, hflip; } device_libcamera_t; @@ -59,9 +60,11 @@ typedef struct buffer_libcamera_s { int libcamera_device_open(device_t *dev); void libcamera_device_close(device_t *dev); void libcamera_device_dump_options(device_t *dev, FILE *stream); +int libcamera_device_dump_options2(device_t *dev, device_option_fn fn, void *opaque); int libcamera_device_set_fps(device_t *dev, int desired_fps); int libcamera_device_set_rotation(device_t *dev, bool vflip, bool hflip); int libcamera_device_set_option(device_t *dev, const char *key, const char *value); +void libcamera_device_apply_controls(device_t *dev); int libcamera_buffer_open(buffer_t *buf); void libcamera_buffer_close(buffer_t *buf); diff --git a/device/libcamera/options.cc b/device/libcamera/options.cc new file mode 100644 index 0000000..f1dbba5 --- /dev/null +++ b/device/libcamera/options.cc @@ -0,0 +1,446 @@ +#ifdef USE_LIBCAMERA +#include "libcamera.hh" +#include "third_party/magic_enum/include/magic_enum.hpp" + +static std::string libcamera_device_option_normalize(std::string key) +{ + key.resize(device_option_normalize_name(key.data(), key.data())); + return key; +} + +template +static std::map control_enum_values(const char *prefix) +{ + std::map ret; + + for (auto e : magic_enum::enum_entries()) { + auto value = std::string(e.second); + if (prefix && value.find(prefix) == 0) { + value = value.substr(strlen(prefix)); + } + ret[e.first] = value; + } + + return ret; +} + +struct libcamera_control_id_t +{ + const libcamera::ControlId *control_id; + std::map enum_values; +}; + +#define LIBCAMERA_CONTROL_RAW(Name, Strip) \ + { Name.id(), { .control_id = &Name, .enum_values = control_enum_values(Strip), } } + +#define LIBCAMERA_CONTROL(Name, Strip) \ + LIBCAMERA_CONTROL_RAW(libcamera::controls::Name, Strip ? Strip : #Name) + +#define LIBCAMERA_DRAFT_CONTROL(Name) \ + LIBCAMERA_CONTROL_RAW(libcamera::controls::draft::Name, #Name) + +static std::map libcamera_control_ids = +{ + LIBCAMERA_CONTROL(AeMeteringMode, "Metering"), + LIBCAMERA_CONTROL(AeConstraintMode, "Constraint"), + LIBCAMERA_CONTROL(AeExposureMode, "Exposure"), + LIBCAMERA_CONTROL(AwbMode, "Awb"), + LIBCAMERA_CONTROL(AfMode, "AfMode"), + LIBCAMERA_CONTROL(AfRange, "AfRange"), + LIBCAMERA_CONTROL(AfSpeed, "AfSpeed"), + LIBCAMERA_CONTROL(AfTrigger, "AfTrigger"), + LIBCAMERA_CONTROL(AfState, "AfState"), + LIBCAMERA_DRAFT_CONTROL(AePrecaptureTrigger), + LIBCAMERA_DRAFT_CONTROL(NoiseReductionMode), + LIBCAMERA_DRAFT_CONTROL(ColorCorrectionAberrationMode), + LIBCAMERA_DRAFT_CONTROL(AeState), + LIBCAMERA_DRAFT_CONTROL(AwbState), + LIBCAMERA_DRAFT_CONTROL(LensShadingMapMode), + LIBCAMERA_DRAFT_CONTROL(SceneFlicker), + LIBCAMERA_DRAFT_CONTROL(TestPatternMode) +}; + +static auto control_type_values = control_enum_values("ControlType"); + +static const std::map *libcamera_find_control_ids(unsigned control_id) +{ + auto iter = libcamera_control_ids.find(control_id); + if (iter == libcamera_control_ids.end()) + return NULL; + + return &iter->second.enum_values; +} + +static long long libcamera_find_control_id_named_value(unsigned control_id, const char *name) +{ + auto named_values = libcamera_find_control_ids(control_id); + if (named_values) { + for (const auto & named_value : *named_values) { + if (device_option_is_equal(named_value.second.c_str(), name)) + return named_value.first; + } + } + + return strtoll(name, NULL, 10); +} + +static libcamera::ControlInfoMap::Map libcamera_control_list(device_t *dev) +{ + libcamera::ControlInfoMap::Map controls_map; + for (auto const &control : dev->libcamera->camera->controls()) { + controls_map[control.first] = control.second; + } + return controls_map; +} + +void libcamera_device_dump_options(device_t *dev, FILE *stream) +{ + auto &properties = dev->libcamera->camera->properties(); + auto idMap = properties.idMap(); + + fprintf(stream, "%s Properties:\n", dev->name); + + for (auto const &control : properties) { + if (!control.first) + continue; + + auto control_id = control.first; + auto control_value = control.second; + std::string control_id_name = ""; + + if (auto control_id_info = idMap ? idMap->at(control_id) : NULL) { + control_id_name = control_id_info->name(); + } + + fprintf(stream, "- property: %s (%08x, type=%s) = %s\n", + control_id_name.c_str(), control_id, + control_type_values[control_value.type()].c_str(), + control_value.toString().c_str()); + } + + fprintf(stream, "\n"); + fprintf(stream, "%s Options:\n", dev->name); + + for (auto const &control : libcamera_control_list(dev)) { + if (!control.first) + continue; + + auto control_id = control.first; + auto control_key = libcamera_device_option_normalize(control_id->name()); + auto control_info = control.second; + + fprintf(stream, "- available option: %s (%08x, type=%s): %s\n", + control_id->name().c_str(), control_id->id(), + control_type_values[control_id->type()].c_str(), + control_info.toString().c_str()); + + auto named_values = libcamera_find_control_ids(control_id->id()); + + if (named_values != NULL) { + for (const auto & named_value : *named_values) { + fprintf(stream, "\t\t%d: %s\n", named_value.first, named_value.second.c_str()); + } + } else { + for (size_t i = 0; i < control_info.values().size(); i++) { + fprintf(stream, "\t\t%s\n", control_info.values()[i].toString().c_str()); + } + } + } + fprintf(stream, "\n"); +} + +static int libcamera_device_dump_control_option(device_option_fn fn, void *opaque, const libcamera::ControlId &control_id, const libcamera::ControlInfo *control_info, const libcamera::ControlValue *control_value, bool read_only) +{ + device_option_t opt = { + .control_id = control_id.id() + }; + opt.flags.read_only = read_only; + strcpy(opt.name, control_id.name().c_str()); + + if (control_info) { + strcpy(opt.description, control_info->toString().c_str()); + } + + if (control_value) { + if (!control_value->isNone()) { + strcpy(opt.value, control_value->toString().c_str()); + } else { + control_value = NULL; + } + } + + switch (control_id.type()) { + case libcamera::ControlTypeNone: + break; + + case libcamera::ControlTypeBool: + opt.type = device_option_type_bool; + break; + + case libcamera::ControlTypeByte: + opt.type = device_option_type_u8; + break; + + case libcamera::ControlTypeInteger32: + opt.type = device_option_type_integer; + break; + + case libcamera::ControlTypeInteger64: + opt.type = device_option_type_integer64; + break; + + case libcamera::ControlTypeFloat: + opt.type = device_option_type_float; + break; + + case libcamera::ControlTypeRectangle: + opt.type = device_option_type_float; + opt.elems = 4; + break; + + case libcamera::ControlTypeSize: + opt.type = device_option_type_float; + opt.elems = 2; + break; + + case libcamera::ControlTypeString: + opt.type = device_option_type_string; + break; + } + + auto named_values = libcamera_find_control_ids(control_id.id()); + + if (named_values != NULL) { + for (const auto & named_value : *named_values) { + if (opt.menu_items >= MAX_DEVICE_OPTION_MENU) { + opt.flags.invalid = true; + break; + } + + device_option_menu_t *opt_menu = &opt.menu[opt.menu_items++]; + opt_menu->id = named_value.first; + strcpy(opt_menu->name, named_value.second.c_str()); + if (control_value && atoi(control_value->toString().c_str()) == opt_menu->id) { + strcpy(opt.value, opt_menu->name); + } + } + } else if (control_info) { + for (size_t i = 0; i < control_info->values().size(); i++) { + if (opt.menu_items >= MAX_DEVICE_OPTION_MENU) { + opt.flags.invalid = true; + break; + } + + device_option_menu_t *opt_menu = &opt.menu[opt.menu_items++]; + opt_menu->id = atoi(control_info->values()[i].toString().c_str()); + strcpy(opt_menu->name, control_info->values()[i].toString().c_str()); + if (control_value && atoi(control_value->toString().c_str()) == opt_menu->id) { + strcpy(opt.value, opt_menu->name); + } + } + } else if (control_value && control_value->numElements() > 0) { + opt.elems = control_value->numElements(); + } + + if (opt.type) { + int ret = fn(&opt, opaque); + if (ret < 0) + return ret; + } + + return 0; +} + +int libcamera_device_dump_options2(device_t *dev, device_option_fn fn, void *opaque) +{ + auto &properties = dev->libcamera->camera->properties(); + auto idMap = properties.idMap(); + + int n = 0; + + for (auto const &control : properties) { + if (!control.first) + continue; + + auto control_id = control.first; + auto control_value = control.second; + std::string control_id_name = ""; + + if (auto control_id_info = idMap ? idMap->at(control_id) : NULL) { + int ret = libcamera_device_dump_control_option(fn, opaque, *control_id_info, NULL, &control_value, true); + if (ret < 0) + return ret; + + n++; + } + } + + for (auto const &control : libcamera_control_list(dev)) { + if (!control.first) + continue; + + auto control_id = control.first; + auto control_info = control.second; + + auto control_value = dev->libcamera->applied_controls.contains(control_id->id()) + ? &dev->libcamera->applied_controls.get(control_id->id()) + : nullptr; + + int ret = libcamera_device_dump_control_option(fn, opaque, + *control_id, &control_info, control_value, false); + if (ret < 0) + return ret; + n++; + } + + return n; +} + +static libcamera::Rectangle libcamera_parse_rectangle(const char *value) +{ + static const char *RECTANGLE_PATTERNS[] = + { + "(%d,%d)/%ux%u", + "%d,%d,%u,%u", + NULL + }; + + for (int i = 0; RECTANGLE_PATTERNS[i]; i++) { + libcamera::Rectangle rectangle; + + if (4 == sscanf(value, RECTANGLE_PATTERNS[i], + &rectangle.x, &rectangle.y, + &rectangle.width, &rectangle.height)) { + return rectangle; + } + } + + return libcamera::Rectangle(); +} + +static libcamera::Size libcamera_parse_size(const char *value) +{ + static const char *SIZE_PATTERNS[] = + { + "%ux%u", + "%u,%u", + NULL + }; + + for (int i = 0; SIZE_PATTERNS[i]; i++) { + libcamera::Size size; + + if (2 == sscanf(value, SIZE_PATTERNS[i], &size.width, &size.height)) { + return size; + } + } + + return libcamera::Size(); +} + +template +static bool libcamera_parse_control_value(libcamera::ControlValue &control_value, const char *value, const F &fn) +{ + std::vector parsed; + + while (value && *value) { + std::string current_value; + + if (const char *next = strchr(value, ',')) { + current_value.assign(value, next); + value = &next[1]; + } else { + current_value.assign(value); + value = NULL; + } + + if (current_value.empty()) + continue; + + parsed.push_back(fn(current_value.c_str())); + } + + if (parsed.empty()) { + return false; + } + + if (parsed.size() > 1) { + control_value.set >(parsed); + } else { + control_value.set(parsed[0]); + } + + return true; +} + +int libcamera_device_set_option(device_t *dev, const char *keyp, const char *value) +{ + for (auto const &control : libcamera_control_list(dev)) { + if (!control.first) + continue; + + auto control_id = control.first; + auto control_key = control_id->name(); + if (!device_option_is_equal(keyp, control_key.c_str())) + continue; + + libcamera::ControlValue control_value; + + switch (control_id->type()) { + case libcamera::ControlTypeNone: + break; + + case libcamera::ControlTypeBool: + control_value.set(atoi(value)); + break; + + case libcamera::ControlTypeByte: + libcamera_parse_control_value(control_value, value, + [control_id](const char *value) { return libcamera_find_control_id_named_value(control_id->id(), value); }); + break; + + case libcamera::ControlTypeInteger32: + libcamera_parse_control_value(control_value, value, + [control_id](const char *value) { return libcamera_find_control_id_named_value(control_id->id(), value); }); + break; + + case libcamera::ControlTypeInteger64: + libcamera_parse_control_value(control_value, value, + [control_id](const char *value) { return libcamera_find_control_id_named_value(control_id->id(), value); }); + break; + + case libcamera::ControlTypeFloat: + libcamera_parse_control_value(control_value, value, atof); + break; + + case libcamera::ControlTypeRectangle: + libcamera_parse_control_value( + control_value, value, libcamera_parse_rectangle); + break; + + case libcamera::ControlTypeSize: + libcamera_parse_control_value( + control_value, value, libcamera_parse_size); + break; + + case libcamera::ControlTypeString: + break; + } + + if (control_value.isNone()) { + LOG_ERROR(dev, "The `%s` type `%d` is not supported.", control_key.c_str(), control_id->type()); + } + + LOG_INFO(dev, "Configuring option '%s' (%08x, type=%d) = %s", + control_key.c_str(), control_id->id(), control_id->type(), + control_value.toString().c_str()); + dev->libcamera->controls.set(control_id->id(), control_value); + return 1; + } + + return 0; + +error: + return -1; +} +#endif // USE_LIBCAMERA diff --git a/device/v4l2/device_options.c b/device/v4l2/device_options.c index a7a064e..e5148b8 100644 --- a/device/v4l2/device_options.c +++ b/device/v4l2/device_options.c @@ -13,7 +13,7 @@ int v4l2_device_set_option_by_id(device_t *dev, const char *name, uint32_t id, i ctl.id = id; ctl.value = value; - LOG_DEBUG(dev, "Configuring option %s (%08x) = %d", name, id, value); + LOG_DEBUG(dev, "Configuring option '%s' (%08x) = %d", name, id, value); ERR_IOCTL(dev, dev->v4l2->subdev_fd >= 0 ? dev->v4l2->subdev_fd : dev->v4l2->dev_fd, VIDIOC_S_CTRL, &ctl, "Can't set option %s", name); return 0; error: @@ -29,8 +29,6 @@ static int v4l2_device_query_control_iter_id(device_t *dev, int fd, uint32_t *id } *id = qctrl.id; - device_option_normalize_name(qctrl.name, qctrl.name); - if (qctrl.flags & V4L2_CTRL_FLAG_DISABLED) { LOG_VERBOSE(dev, "The '%s' is disabled", qctrl.name); return 0; @@ -39,7 +37,7 @@ static int v4l2_device_query_control_iter_id(device_t *dev, int fd, uint32_t *id return 0; } - LOG_VERBOSE(dev, "Available control: %s (%08x, type=%d)", + LOG_VERBOSE(dev, "Available control: '%s' (%08x, type=%d)", qctrl.name, qctrl.id, qctrl.type); dev->v4l2->controls = reallocarray(dev->v4l2->controls, dev->v4l2->ncontrols+1, sizeof(device_v4l2_control_t)); @@ -71,10 +69,8 @@ int v4l2_device_set_option(device_t *dev, const char *key, const char *value) device_v4l2_control_t *control = NULL; int ret = -1; - device_option_normalize_name(keyp, keyp); - for (int i = 0; i < dev->v4l2->ncontrols; i++) { - if (strcmp(dev->v4l2->controls[i].control.name, keyp) == 0) { + if (device_option_is_equal(dev->v4l2->controls[i].control.name, keyp)) { control = &dev->v4l2->controls[i]; break; } @@ -87,14 +83,40 @@ int v4l2_device_set_option(device_t *dev, const char *key, const char *value) switch(control->control.type) { case V4L2_CTRL_TYPE_INTEGER: + case V4L2_CTRL_TYPE_INTEGER_MENU: case V4L2_CTRL_TYPE_BOOLEAN: + { + struct v4l2_control ctl = { + .id = control->control.id, + .value = atoi(value) + }; + LOG_INFO(dev, "Configuring option '%s' (%08x) = %d", control->control.name, ctl.id, ctl.value); + ERR_IOCTL(dev, control->fd, VIDIOC_S_CTRL, &ctl, "Can't set option %s", control->control.name); + ret = 1; + } + break; + case V4L2_CTRL_TYPE_MENU: { struct v4l2_control ctl = { .id = control->control.id, .value = atoi(value) }; - LOG_INFO(dev, "Configuring option %s (%08x) = %d", control->control.name, ctl.id, ctl.value); + + for (int j = control->control.minimum; j <= control->control.maximum; j++) { + struct v4l2_querymenu menu = { + .id = control->control.id, + .index = j + }; + + if (0 == ioctl(control->fd, VIDIOC_QUERYMENU, &menu)) { + if (device_option_is_equal(valuep, (const char*)menu.name)) { + ctl.value = j; + break; + } + } + } + LOG_INFO(dev, "Configuring option '%s' (%08x) = %d", control->control.name, ctl.id, ctl.value); ERR_IOCTL(dev, control->fd, VIDIOC_S_CTRL, &ctl, "Can't set option %s", control->control.name); ret = 1; } @@ -138,7 +160,7 @@ int v4l2_device_set_option(device_t *dev, const char *key, const char *value) } } - LOG_INFO(dev, "Configuring option %s (%08x) = [%d tokens, expected %d]", + LOG_INFO(dev, "Configuring option '%s' (%08x) = [%d tokens, expected %d]", control->control.name, ctl.id, tokens, control->control.elems); ERR_IOCTL(dev, control->fd, VIDIOC_S_EXT_CTRLS, &ctrls, "Can't set option %s", control->control.name); ret = 1; @@ -163,8 +185,11 @@ void v4l2_device_dump_options(device_t *dev, FILE *stream) for (int i = 0; i < dev->v4l2->ncontrols; i++) { device_v4l2_control_t *control = &dev->v4l2->controls[i]; + char name[512]; + device_option_normalize_name(control->control.name, name); + fprintf(stream, "- available option: %s (%08x, type=%d): ", - control->control.name, control->control.id, control->control.type); + name, control->control.id, control->control.type); switch(control->control.type) { case V4L2_CTRL_TYPE_U8: @@ -210,3 +235,163 @@ void v4l2_device_dump_options(device_t *dev, FILE *stream) } fprintf(stream, "\n"); } + +int v4l2_device_dump_options2(device_t *dev, device_option_fn fn, void *opaque) +{ + int n = 0; + + for (int i = 0; i < dev->v4l2->ncontrols; i++) { + device_v4l2_control_t *control = &dev->v4l2->controls[i]; + + device_option_t opt = { + .control_id = control->control.id, + .elems = control->control.elems, + }; + strcpy(opt.name, control->control.name); + + char buf[8192]; + + struct v4l2_ext_control ext_control = { + .id = control->control.id, + .size = sizeof(buf), + .ptr = buf, + }; + + struct v4l2_ext_controls ext_controls = { + .ctrl_class = 0, + .which = V4L2_CTRL_WHICH_CUR_VAL, + .count = 1, + .controls = &ext_control + }; + + bool ext_control_valid = ioctl(control->fd, VIDIOC_G_EXT_CTRLS, &ext_controls) == 0; + + switch(control->control.type) { + case V4L2_CTRL_TYPE_U8: + opt.type = device_option_type_u8; + snprintf(opt.description, sizeof(opt.description), "[%02x..%02x]", + (__u8)control->control.minimum & 0xFF, (__u8)control->control.maximum & 0xFF); + if (ext_control_valid) { + int n = 0; + for (int i = 0; i < ext_control.size; i++) { + if (n + 3 >= sizeof(opt.value)) break; + if (i) opt.value[n++] = ' '; + n += sprintf(opt.value + n, "%02x", ext_control.p_u8[i]&0xFF); + } + } + break; + + case V4L2_CTRL_TYPE_U16: + opt.type = device_option_type_u16; + snprintf(opt.description, sizeof(opt.description), "[%04x..%04x]", + (__u16)control->control.minimum & 0xFFFF, (__u16)control->control.maximum & 0xFFFF); + if (ext_control_valid) { + int n = 0; + for (int i = 0; i < ext_control.size; i++) { + if (n + 5 >= sizeof(opt.value)) break; + if (i) opt.value[n++] = ' '; + n += sprintf(opt.value + n, "%04x", ext_control.p_u16[i]&0xFFFF); + } + } + break; + + case V4L2_CTRL_TYPE_U32: + opt.type = device_option_type_u32; + snprintf(opt.description, sizeof(opt.description), "[%lld..%lld]", + control->control.minimum, control->control.maximum); + if (ext_control_valid) { + int n = 0; + for (int i = 0; i < ext_control.size; i++) { + if (n + 9 >= sizeof(opt.value)) break; + if (i) opt.value[n++] = ' '; + n += sprintf(opt.value + n, "%08x", ext_control.p_u32[i]); + } + } + break; + + case V4L2_CTRL_TYPE_BOOLEAN: + opt.type = device_option_type_bool; + snprintf(opt.description, sizeof(opt.description), "[%lld..%lld]", + control->control.minimum, control->control.maximum); + if (ext_control_valid) + snprintf(opt.value, sizeof(opt.value), "%d", ext_control.value ? 1 : 0); + break; + + case V4L2_CTRL_TYPE_INTEGER: + opt.type = device_option_type_integer; + snprintf(opt.description, sizeof(opt.description), "[%lld..%lld]", + control->control.minimum, control->control.maximum); + if (ext_control_valid) + snprintf(opt.value, sizeof(opt.value), "%d", ext_control.value); + break; + + case V4L2_CTRL_TYPE_INTEGER64: + opt.type = device_option_type_integer64; + snprintf(opt.description, sizeof(opt.description), "[%lld..%lld]", + control->control.minimum, control->control.maximum); + if (ext_control_valid) + snprintf(opt.value, sizeof(opt.value), "%lld", ext_control.value64); + break; + + case V4L2_CTRL_TYPE_BUTTON: + opt.type = device_option_type_bool; + snprintf(opt.description, sizeof(opt.description), "button"); + break; + + case V4L2_CTRL_TYPE_MENU: + case V4L2_CTRL_TYPE_INTEGER_MENU: + opt.type = device_option_type_integer; + snprintf(opt.description, sizeof(opt.description), "[%lld..%lld]", + control->control.minimum, control->control.maximum); + + if (ext_control_valid) + snprintf(opt.value, sizeof(opt.value), "%d", ext_control.value); + + for (int j = control->control.minimum; j <= control->control.maximum; j++) { + struct v4l2_querymenu menu = { + .id = control->control.id, + .index = j + }; + + if (0 != ioctl(control->fd, VIDIOC_QUERYMENU, &menu)) + continue; + + if (opt.menu_items >= MAX_DEVICE_OPTION_MENU) { + opt.flags.invalid = true; + break; + } + + device_option_menu_t *opt_menu = &opt.menu[opt.menu_items++]; + opt_menu->id = j; + + if (control->control.type == V4L2_CTRL_TYPE_MENU) { + snprintf(opt_menu->name, sizeof(opt_menu->name), "%s", menu.name); + if (ext_control_valid && opt_menu->id == ext_control.value) + snprintf(opt.value, sizeof(opt.value), "%s", menu.name); + } else { + snprintf(opt_menu->name, sizeof(opt_menu->name), "%lld", menu.value); + } + } + break; + + case V4L2_CTRL_TYPE_STRING: + opt.type = device_option_type_string; + if (ext_control_valid) + snprintf(opt.value, sizeof(opt.value), "%s", ext_control.string); + break; + + default: + opt.flags.invalid = true; // unsupported + break; + } + + if (opt.type) { + int ret = fn(&opt, opaque); + if (ret < 0) + return ret; + n++; + } + } + + return n; +} diff --git a/device/v4l2/v4l2.c b/device/v4l2/v4l2.c index e918db1..80423a1 100644 --- a/device/v4l2/v4l2.c +++ b/device/v4l2/v4l2.c @@ -6,6 +6,7 @@ device_hw_t v4l2_device_hw = { .device_close = v4l2_device_close, .device_video_force_key = v4l2_device_video_force_key, .device_dump_options = v4l2_device_dump_options, + .device_dump_options2 = v4l2_device_dump_options2, .device_set_fps = v4l2_device_set_fps, .device_set_option = v4l2_device_set_option, diff --git a/device/v4l2/v4l2.h b/device/v4l2/v4l2.h index c71d5be..1347868 100644 --- a/device/v4l2/v4l2.h +++ b/device/v4l2/v4l2.h @@ -9,9 +9,12 @@ typedef struct buffer_s buffer_t; typedef struct buffer_list_s buffer_list_t; +typedef struct device_option_s device_option_t; typedef struct device_s device_t; struct pollfd; +typedef int device_option_fn(device_option_t *option, void *opaque); + typedef struct device_v4l2_control_s { int fd; struct v4l2_query_ext_ctrl control; @@ -38,6 +41,7 @@ int v4l2_device_open(device_t *dev); void v4l2_device_close(device_t *dev); int v4l2_device_video_force_key(device_t *dev); void v4l2_device_dump_options(device_t *dev, FILE *stream); +int v4l2_device_dump_options2(device_t *dev, device_option_fn fn, void *opaque); int v4l2_device_set_fps(device_t *dev, int desired_fps); int v4l2_device_set_option(device_t *dev, const char *key, const char *value); diff --git a/third_party/magic_enum b/third_party/magic_enum new file mode 160000 index 0000000..48054f6 --- /dev/null +++ b/third_party/magic_enum @@ -0,0 +1 @@ +Subproject commit 48054f64abcfc83f58d9aa2efbbd21604f93aca4 diff --git a/util/opts/control.c b/util/opts/control.c index 04aa800..b8a9a28 100644 --- a/util/opts/control.c +++ b/util/opts/control.c @@ -4,17 +4,23 @@ #include #include +static bool isalnum_dot(char c) +{ + return isalnum(c) || c == '.'; +} + int device_option_normalize_name(const char *in, char *outp) { // The output is always shorter, so `outp=in` // colour_correction_matrix => colourcorrectionmatrix // Colour Correction Matrix => colourcorrectionmatrix // ColourCorrectionMatrix => colourcorrectionmatrix + // Colour.Correction.Matrix => colour.correction.matrix char *out = outp; while (*in) { - if (isalnum(*in)) { + if (isalnum_dot(*in)) { *out++ = tolower(*in++); } else { in++; @@ -22,5 +28,26 @@ int device_option_normalize_name(const char *in, char *outp) } *out++ = 0; - return out - outp; + return out - outp - 1; +} + +bool device_option_is_equal(const char *a, const char *b) +{ + // Similar to device_option_normalize_name + // but ignore case sensitiveness + + while (*a || *b) { + while (*a && !isalnum_dot(*a)) + a++; + while (*b && !isalnum_dot(*b)) + b++; + + if (tolower(*a) != tolower(*b)) + return 0; + + a++; + b++; + } + + return 1; } diff --git a/util/opts/control.h b/util/opts/control.h index fd4da10..b442684 100644 --- a/util/opts/control.h +++ b/util/opts/control.h @@ -1 +1,4 @@ +#include + int device_option_normalize_name(const char *in, char *out); +bool device_option_is_equal(const char *a, const char *b);