Add common, media_ctrl and v4l2_ctrl modules with CLI drivers and docs
Modules (src/modules/): - common/error: App_Error struct with structured detail union, error codes, app_error_print(); designed for future upgrade to preprocessor-generated location codes - media_ctrl: media device enumeration, topology query (entities/pads/links), link enable/disable via Media Controller API (/dev/media*) - v4l2_ctrl: control enumeration (with menu item fetching), get/set via V4L2 ext controls API, device discovery (/dev/video*) All modules use -std=c11 -D_GNU_SOURCE, build artifacts go to build/ only. Kernel-version-dependent constants guarded with #ifdef + #warning. CLI drivers (dev/cli/): - media_ctrl_cli: list, info, topology, set-link subcommands - v4l2_ctrl_cli: list, controls, get, set subcommands Docs (docs/cli/): - media_ctrl_cli.md and v4l2_ctrl_cli.md with usage, examples, and context within the video routing system Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1 +1,2 @@
|
|||||||
reference/
|
reference/
|
||||||
|
build/
|
||||||
|
|||||||
9
Makefile
Normal file
9
Makefile
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
.PHONY: all clean cli
|
||||||
|
|
||||||
|
all: cli
|
||||||
|
|
||||||
|
cli:
|
||||||
|
$(MAKE) -C dev/cli
|
||||||
|
|
||||||
|
clean:
|
||||||
|
rm -rf build/
|
||||||
@@ -2,7 +2,8 @@
|
|||||||
|
|
||||||
## Language
|
## Language
|
||||||
|
|
||||||
- **C11** throughout
|
- **C11** throughout (`-std=c11`)
|
||||||
|
- **`_GNU_SOURCE`** defined via `-D_GNU_SOURCE` in all Makefiles — enables the full GNU/Linux feature set (POSIX 2008, BSD, GNU and Linux-specific extensions); do not define this in source files
|
||||||
- Target platform: Linux (V4L2, epoll, etc. are Linux-specific)
|
- Target platform: Linux (V4L2, epoll, etc. are Linux-specific)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
29
dev/cli/Makefile
Normal file
29
dev/cli/Makefile
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
ROOT := $(abspath ../..)
|
||||||
|
CC = gcc
|
||||||
|
CFLAGS = -std=c11 -Wall -Wextra -D_GNU_SOURCE -I$(ROOT)/include
|
||||||
|
BUILD = $(ROOT)/build
|
||||||
|
|
||||||
|
COMMON_OBJ = $(BUILD)/common/error.o
|
||||||
|
MEDIA_CTRL_OBJ = $(BUILD)/media_ctrl/media_ctrl.o
|
||||||
|
V4L2_CTRL_OBJ = $(BUILD)/v4l2_ctrl/v4l2_ctrl.o
|
||||||
|
|
||||||
|
.PHONY: all clean modules
|
||||||
|
|
||||||
|
all: modules $(BUILD)/cli/media_ctrl_cli $(BUILD)/cli/v4l2_ctrl_cli
|
||||||
|
|
||||||
|
modules:
|
||||||
|
$(MAKE) -C $(ROOT)/src/modules/common
|
||||||
|
$(MAKE) -C $(ROOT)/src/modules/media_ctrl
|
||||||
|
$(MAKE) -C $(ROOT)/src/modules/v4l2_ctrl
|
||||||
|
|
||||||
|
$(BUILD)/cli/media_ctrl_cli: media_ctrl_cli.c $(COMMON_OBJ) $(MEDIA_CTRL_OBJ) | $(BUILD)/cli
|
||||||
|
$(CC) $(CFLAGS) -o $@ $^
|
||||||
|
|
||||||
|
$(BUILD)/cli/v4l2_ctrl_cli: v4l2_ctrl_cli.c $(COMMON_OBJ) $(V4L2_CTRL_OBJ) | $(BUILD)/cli
|
||||||
|
$(CC) $(CFLAGS) -o $@ $^
|
||||||
|
|
||||||
|
$(BUILD)/cli:
|
||||||
|
mkdir -p $@
|
||||||
|
|
||||||
|
clean:
|
||||||
|
rm -f $(BUILD)/cli/media_ctrl_cli $(BUILD)/cli/v4l2_ctrl_cli
|
||||||
248
dev/cli/media_ctrl_cli.c
Normal file
248
dev/cli/media_ctrl_cli.c
Normal file
@@ -0,0 +1,248 @@
|
|||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
#include "error.h"
|
||||||
|
#include "media_ctrl.h"
|
||||||
|
|
||||||
|
static void usage(const char *prog) {
|
||||||
|
fprintf(stderr,
|
||||||
|
"Usage: %s <command> [args]\n"
|
||||||
|
"\n"
|
||||||
|
"Commands:\n"
|
||||||
|
" list List all /dev/media* devices\n"
|
||||||
|
" info <device> Show device information\n"
|
||||||
|
" topology <device> Show full pipeline topology\n"
|
||||||
|
" set-link <device> <src_entity>:<src_pad> <sink_entity>:<sink_pad> <0|1>\n"
|
||||||
|
" Enable (1) or disable (0) a link\n"
|
||||||
|
"\n"
|
||||||
|
"Examples:\n"
|
||||||
|
" %s list\n"
|
||||||
|
" %s info /dev/media0\n"
|
||||||
|
" %s topology /dev/media0\n"
|
||||||
|
" %s set-link /dev/media0 1:0 2:0 1\n",
|
||||||
|
prog, prog, prog, prog, prog);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- list --- */
|
||||||
|
|
||||||
|
static void on_device_found(const char *path, void *userdata) {
|
||||||
|
(void)userdata;
|
||||||
|
printf(" %s\n", path);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int cmd_list(void) {
|
||||||
|
printf("Media devices:\n");
|
||||||
|
struct App_Error err = media_ctrl_find_devices(on_device_found, NULL);
|
||||||
|
if (!APP_IS_OK(err)) {
|
||||||
|
app_error_print(&err);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- info --- */
|
||||||
|
|
||||||
|
static int cmd_info(const char *device) {
|
||||||
|
struct Media_Ctrl *ctrl = NULL;
|
||||||
|
struct App_Error err = media_ctrl_open(device, &ctrl);
|
||||||
|
if (!APP_IS_OK(err)) {
|
||||||
|
app_error_print(&err);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Media_Device_Info info;
|
||||||
|
err = media_ctrl_get_info(ctrl, &info);
|
||||||
|
if (!APP_IS_OK(err)) {
|
||||||
|
app_error_print(&err);
|
||||||
|
media_ctrl_close(ctrl);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
printf("Device: %s\n", device);
|
||||||
|
printf("Driver: %s\n", info.driver);
|
||||||
|
printf("Model: %s\n", info.model);
|
||||||
|
printf("Serial: %s\n", info.serial);
|
||||||
|
printf("Bus info: %s\n", info.bus_info);
|
||||||
|
printf("Media version: %u.%u.%u\n",
|
||||||
|
(info.media_version >> 16) & 0xff,
|
||||||
|
(info.media_version >> 8) & 0xff,
|
||||||
|
info.media_version & 0xff);
|
||||||
|
printf("Hardware rev: 0x%08x\n", info.hw_revision);
|
||||||
|
printf("Driver version: %u.%u.%u\n",
|
||||||
|
(info.driver_version >> 16) & 0xff,
|
||||||
|
(info.driver_version >> 8) & 0xff,
|
||||||
|
info.driver_version & 0xff);
|
||||||
|
|
||||||
|
media_ctrl_close(ctrl);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- topology --- */
|
||||||
|
|
||||||
|
struct Topology_State {
|
||||||
|
struct Media_Ctrl *ctrl;
|
||||||
|
};
|
||||||
|
|
||||||
|
static void on_pad(const struct Media_Pad *pad, void *userdata) {
|
||||||
|
(void)userdata;
|
||||||
|
printf(" pad %u: %s\n", pad->index, media_pad_flag_name(pad->flags));
|
||||||
|
}
|
||||||
|
|
||||||
|
static void on_link(const struct Media_Link *link, void *userdata) {
|
||||||
|
(void)userdata;
|
||||||
|
const char *enabled = (link->flags & MEDIA_LNK_FL_ENABLED) ? "enabled" : "disabled";
|
||||||
|
const char *mutable = (link->flags & MEDIA_LNK_FL_IMMUTABLE) ? ", immutable" : "";
|
||||||
|
printf(" link: entity %u pad %u -> entity %u pad %u [%s%s]\n",
|
||||||
|
link->source.entity_id, link->source.index,
|
||||||
|
link->sink.entity_id, link->sink.index,
|
||||||
|
enabled, mutable);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void on_entity(const struct Media_Entity *entity, void *userdata) {
|
||||||
|
struct Topology_State *state = userdata;
|
||||||
|
|
||||||
|
printf(" Entity %u: %s\n", entity->id, entity->name);
|
||||||
|
printf(" type: %s (0x%08x)\n", media_entity_type_name(entity->type), entity->type);
|
||||||
|
printf(" flags: 0x%08x\n", entity->flags);
|
||||||
|
printf(" pads: %u links: %u\n", entity->pad_count, entity->link_count);
|
||||||
|
|
||||||
|
if (entity->dev_major != 0 || entity->dev_minor != 0) {
|
||||||
|
printf(" device: %u:%u\n", entity->dev_major, entity->dev_minor);
|
||||||
|
}
|
||||||
|
|
||||||
|
struct App_Error err = media_ctrl_enum_entity_pads_and_links(
|
||||||
|
state->ctrl, entity, on_pad, on_link, NULL);
|
||||||
|
if (!APP_IS_OK(err)) {
|
||||||
|
fprintf(stderr, " (error enumerating pads/links: ");
|
||||||
|
app_error_print(&err);
|
||||||
|
fprintf(stderr, ")\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static int cmd_topology(const char *device) {
|
||||||
|
struct Media_Ctrl *ctrl = NULL;
|
||||||
|
struct App_Error err = media_ctrl_open(device, &ctrl);
|
||||||
|
if (!APP_IS_OK(err)) {
|
||||||
|
app_error_print(&err);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Media_Device_Info info;
|
||||||
|
err = media_ctrl_get_info(ctrl, &info);
|
||||||
|
if (!APP_IS_OK(err)) {
|
||||||
|
app_error_print(&err);
|
||||||
|
media_ctrl_close(ctrl);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
printf("Device: %s (%s)\n\n", info.model, device);
|
||||||
|
|
||||||
|
struct Topology_State state = { .ctrl = ctrl };
|
||||||
|
err = media_ctrl_enum_entities(ctrl, on_entity, &state);
|
||||||
|
if (!APP_IS_OK(err)) {
|
||||||
|
app_error_print(&err);
|
||||||
|
media_ctrl_close(ctrl);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
media_ctrl_close(ctrl);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- set-link --- */
|
||||||
|
|
||||||
|
static int parse_entity_pad(const char *s, uint32_t *entity_out, uint16_t *pad_out) {
|
||||||
|
unsigned int entity, pad;
|
||||||
|
if (sscanf(s, "%u:%u", &entity, &pad) != 2) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
*entity_out = (uint32_t)entity;
|
||||||
|
*pad_out = (uint16_t)pad;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int cmd_set_link(
|
||||||
|
const char *device,
|
||||||
|
const char *src_arg,
|
||||||
|
const char *sink_arg,
|
||||||
|
const char *enabled_arg)
|
||||||
|
{
|
||||||
|
uint32_t src_entity, sink_entity;
|
||||||
|
uint16_t src_pad, sink_pad;
|
||||||
|
|
||||||
|
if (parse_entity_pad(src_arg, &src_entity, &src_pad) < 0) {
|
||||||
|
fprintf(stderr, "Invalid source: '%s' (expected entity:pad)\n", src_arg);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if (parse_entity_pad(sink_arg, &sink_entity, &sink_pad) < 0) {
|
||||||
|
fprintf(stderr, "Invalid sink: '%s' (expected entity:pad)\n", sink_arg);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
int enabled = atoi(enabled_arg);
|
||||||
|
|
||||||
|
struct Media_Ctrl *ctrl = NULL;
|
||||||
|
struct App_Error err = media_ctrl_open(device, &ctrl);
|
||||||
|
if (!APP_IS_OK(err)) {
|
||||||
|
app_error_print(&err);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
err = media_ctrl_set_link(ctrl, src_entity, src_pad, sink_entity, sink_pad, enabled);
|
||||||
|
if (!APP_IS_OK(err)) {
|
||||||
|
app_error_print(&err);
|
||||||
|
media_ctrl_close(ctrl);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
printf("Link %u:%u -> %u:%u %s\n",
|
||||||
|
src_entity, src_pad, sink_entity, sink_pad,
|
||||||
|
enabled ? "enabled" : "disabled");
|
||||||
|
|
||||||
|
media_ctrl_close(ctrl);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- main --- */
|
||||||
|
|
||||||
|
int main(int argc, char **argv) {
|
||||||
|
if (argc < 2) {
|
||||||
|
usage(argv[0]);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
const char *cmd = argv[1];
|
||||||
|
|
||||||
|
if (strcmp(cmd, "list") == 0) {
|
||||||
|
return cmd_list();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (strcmp(cmd, "info") == 0) {
|
||||||
|
if (argc < 3) {
|
||||||
|
fprintf(stderr, "info requires a device argument\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return cmd_info(argv[2]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (strcmp(cmd, "topology") == 0) {
|
||||||
|
if (argc < 3) {
|
||||||
|
fprintf(stderr, "topology requires a device argument\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return cmd_topology(argv[2]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (strcmp(cmd, "set-link") == 0) {
|
||||||
|
if (argc < 6) {
|
||||||
|
fprintf(stderr, "set-link requires: <device> <src_entity>:<src_pad> <sink_entity>:<sink_pad> <0|1>\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return cmd_set_link(argv[2], argv[3], argv[4], argv[5]);
|
||||||
|
}
|
||||||
|
|
||||||
|
fprintf(stderr, "Unknown command: %s\n\n", cmd);
|
||||||
|
usage(argv[0]);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
206
dev/cli/v4l2_ctrl_cli.c
Normal file
206
dev/cli/v4l2_ctrl_cli.c
Normal file
@@ -0,0 +1,206 @@
|
|||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
#include "error.h"
|
||||||
|
#include "v4l2_ctrl.h"
|
||||||
|
|
||||||
|
static void usage(const char *prog) {
|
||||||
|
fprintf(stderr,
|
||||||
|
"Usage: %s <command> [args]\n"
|
||||||
|
"\n"
|
||||||
|
"Commands:\n"
|
||||||
|
" list List all /dev/video* devices\n"
|
||||||
|
" controls <device> List all controls with current values\n"
|
||||||
|
" get <device> <id> Get a control value by ID (decimal or 0x hex)\n"
|
||||||
|
" set <device> <id>=<value>\n"
|
||||||
|
" Set a control value\n"
|
||||||
|
"\n"
|
||||||
|
"Examples:\n"
|
||||||
|
" %s list\n"
|
||||||
|
" %s controls /dev/video0\n"
|
||||||
|
" %s get /dev/video0 0x00980900\n"
|
||||||
|
" %s set /dev/video0 0x00980900=128\n",
|
||||||
|
prog, prog, prog, prog, prog);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- list --- */
|
||||||
|
|
||||||
|
static void on_device_found(const char *path, void *userdata) {
|
||||||
|
(void)userdata;
|
||||||
|
printf(" %s\n", path);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int cmd_list(void) {
|
||||||
|
printf("Video devices:\n");
|
||||||
|
struct App_Error err = v4l2_ctrl_find_devices(on_device_found, NULL);
|
||||||
|
if (!APP_IS_OK(err)) {
|
||||||
|
app_error_print(&err);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- controls --- */
|
||||||
|
|
||||||
|
static void on_control(
|
||||||
|
const struct V4l2_Ctrl_Desc *desc,
|
||||||
|
uint32_t menu_count,
|
||||||
|
const struct V4l2_Menu_Item *menu_items,
|
||||||
|
void *userdata)
|
||||||
|
{
|
||||||
|
(void)userdata;
|
||||||
|
|
||||||
|
printf(" 0x%08x %-32s %-9s current=%-6d default=%-6d min=%-6d max=%-6d",
|
||||||
|
desc->id,
|
||||||
|
desc->name,
|
||||||
|
v4l2_ctrl_type_name(desc->type),
|
||||||
|
desc->current_value,
|
||||||
|
desc->default_value,
|
||||||
|
desc->min,
|
||||||
|
desc->max);
|
||||||
|
|
||||||
|
if (desc->type == CTRL_TYPE_INTEGER || desc->type == CTRL_TYPE_INTEGER64) {
|
||||||
|
printf(" step=%d", desc->step);
|
||||||
|
}
|
||||||
|
|
||||||
|
printf("\n");
|
||||||
|
|
||||||
|
for (uint32_t i = 0; i < menu_count; i++) {
|
||||||
|
if (desc->type == CTRL_TYPE_INTEGER_MENU) {
|
||||||
|
printf(" [%u] %lld\n", menu_items[i].index, (long long)menu_items[i].value);
|
||||||
|
} else {
|
||||||
|
printf(" [%u] %s\n", menu_items[i].index, menu_items[i].name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static int cmd_controls(const char *device) {
|
||||||
|
struct V4l2_Ctrl_Handle *handle = NULL;
|
||||||
|
struct App_Error err = v4l2_ctrl_open(device, &handle);
|
||||||
|
if (!APP_IS_OK(err)) {
|
||||||
|
app_error_print(&err);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
printf("Controls on %s:\n", device);
|
||||||
|
err = v4l2_ctrl_enumerate(handle, on_control, NULL);
|
||||||
|
if (!APP_IS_OK(err)) {
|
||||||
|
app_error_print(&err);
|
||||||
|
v4l2_ctrl_close(handle);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
v4l2_ctrl_close(handle);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- get --- */
|
||||||
|
|
||||||
|
static int cmd_get(const char *device, const char *id_arg) {
|
||||||
|
uint32_t id = (uint32_t)strtoul(id_arg, NULL, 0);
|
||||||
|
|
||||||
|
struct V4l2_Ctrl_Handle *handle = NULL;
|
||||||
|
struct App_Error err = v4l2_ctrl_open(device, &handle);
|
||||||
|
if (!APP_IS_OK(err)) {
|
||||||
|
app_error_print(&err);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
int32_t value;
|
||||||
|
err = v4l2_ctrl_get(handle, id, &value);
|
||||||
|
if (!APP_IS_OK(err)) {
|
||||||
|
app_error_print(&err);
|
||||||
|
v4l2_ctrl_close(handle);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
printf("0x%08x = %d\n", id, value);
|
||||||
|
|
||||||
|
v4l2_ctrl_close(handle);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- set --- */
|
||||||
|
|
||||||
|
static int cmd_set(const char *device, const char *assignment) {
|
||||||
|
char id_str[64];
|
||||||
|
const char *eq = strchr(assignment, '=');
|
||||||
|
if (!eq) {
|
||||||
|
fprintf(stderr, "Expected <id>=<value>, got: %s\n", assignment);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t id_len = (size_t)(eq - assignment);
|
||||||
|
if (id_len >= sizeof(id_str)) {
|
||||||
|
fprintf(stderr, "ID string too long\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
memcpy(id_str, assignment, id_len);
|
||||||
|
id_str[id_len] = '\0';
|
||||||
|
|
||||||
|
uint32_t id = (uint32_t)strtoul(id_str, NULL, 0);
|
||||||
|
int32_t value = (int32_t)strtol(eq + 1, NULL, 0);
|
||||||
|
|
||||||
|
struct V4l2_Ctrl_Handle *handle = NULL;
|
||||||
|
struct App_Error err = v4l2_ctrl_open(device, &handle);
|
||||||
|
if (!APP_IS_OK(err)) {
|
||||||
|
app_error_print(&err);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
err = v4l2_ctrl_set(handle, id, value);
|
||||||
|
if (!APP_IS_OK(err)) {
|
||||||
|
app_error_print(&err);
|
||||||
|
v4l2_ctrl_close(handle);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
printf("0x%08x set to %d\n", id, value);
|
||||||
|
|
||||||
|
v4l2_ctrl_close(handle);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- main --- */
|
||||||
|
|
||||||
|
int main(int argc, char **argv) {
|
||||||
|
if (argc < 2) {
|
||||||
|
usage(argv[0]);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
const char *cmd = argv[1];
|
||||||
|
|
||||||
|
if (strcmp(cmd, "list") == 0) {
|
||||||
|
return cmd_list();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (strcmp(cmd, "controls") == 0) {
|
||||||
|
if (argc < 3) {
|
||||||
|
fprintf(stderr, "controls requires a device argument\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return cmd_controls(argv[2]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (strcmp(cmd, "get") == 0) {
|
||||||
|
if (argc < 4) {
|
||||||
|
fprintf(stderr, "get requires: <device> <id>\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return cmd_get(argv[2], argv[3]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (strcmp(cmd, "set") == 0) {
|
||||||
|
if (argc < 4) {
|
||||||
|
fprintf(stderr, "set requires: <device> <id>=<value>\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return cmd_set(argv[2], argv[3]);
|
||||||
|
}
|
||||||
|
|
||||||
|
fprintf(stderr, "Unknown command: %s\n\n", cmd);
|
||||||
|
usage(argv[0]);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
129
docs/cli/media_ctrl_cli.md
Normal file
129
docs/cli/media_ctrl_cli.md
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
# media_ctrl_cli
|
||||||
|
|
||||||
|
A development tool for exploring and configuring the Linux Media Controller pipeline. It wraps the `media_ctrl` module and provides a command-line interface to the Media Controller API (`/dev/media*`).
|
||||||
|
|
||||||
|
This tool is the counterpart to `media-ctl` from the `v4l-utils` package. It covers the same ground but is built on our own `media_ctrl` translation unit.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Build
|
||||||
|
|
||||||
|
From the repository root:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
make
|
||||||
|
```
|
||||||
|
|
||||||
|
Or from `dev/cli/` directly:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
make media_ctrl_cli
|
||||||
|
```
|
||||||
|
|
||||||
|
The binary is placed in `dev/cli/`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
### `list`
|
||||||
|
|
||||||
|
Enumerate all media devices present on the system.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
./media_ctrl_cli list
|
||||||
|
```
|
||||||
|
|
||||||
|
Example output:
|
||||||
|
|
||||||
|
```
|
||||||
|
Media devices:
|
||||||
|
/dev/media0
|
||||||
|
/dev/media1
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `info <device>`
|
||||||
|
|
||||||
|
Show identification information for a media device.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
./media_ctrl_cli info /dev/media0
|
||||||
|
```
|
||||||
|
|
||||||
|
Example output:
|
||||||
|
|
||||||
|
```
|
||||||
|
Device: /dev/media0
|
||||||
|
Driver: unicam
|
||||||
|
Model: unicam
|
||||||
|
Serial:
|
||||||
|
Bus info: platform:fe801000.csi
|
||||||
|
Media version: 5.15.0
|
||||||
|
Hardware rev: 0x00000000
|
||||||
|
Driver version: 5.15.0
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `topology <device>`
|
||||||
|
|
||||||
|
Show the full pipeline topology: all entities, their pads, and all links between them. This is the main tool for understanding how a camera pipeline is structured.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
./media_ctrl_cli topology /dev/media0
|
||||||
|
```
|
||||||
|
|
||||||
|
Example output on a Raspberry Pi with an IMX477 camera:
|
||||||
|
|
||||||
|
```
|
||||||
|
Device: unicam (/dev/media0)
|
||||||
|
|
||||||
|
Entity 1: imx477 4-001a
|
||||||
|
type: v4l2-subdev (0x00020001)
|
||||||
|
flags: 0x00000000
|
||||||
|
pads: 1 links: 1
|
||||||
|
pad 0: source
|
||||||
|
link: entity 1 pad 0 -> entity 2 pad 0 [enabled]
|
||||||
|
|
||||||
|
Entity 2: unicam-image
|
||||||
|
type: devnode (0x00010001)
|
||||||
|
flags: 0x00000000
|
||||||
|
pads: 1 links: 1
|
||||||
|
device: 81:1
|
||||||
|
pad 0: sink
|
||||||
|
link: entity 1 pad 0 -> entity 2 pad 0 [enabled]
|
||||||
|
```
|
||||||
|
|
||||||
|
Each entity shows:
|
||||||
|
- **type** — the kernel entity function code and its human-readable name
|
||||||
|
- **pads** — each pad with its direction (source or sink)
|
||||||
|
- **links** — connections to other entity pads, with enabled/disabled state and whether the link is immutable
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `set-link <device> <src_entity>:<src_pad> <sink_entity>:<sink_pad> <0|1>`
|
||||||
|
|
||||||
|
Enable or disable a link between two pads. Entity IDs and pad indices are the numeric values shown in the `topology` output.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# Enable a link
|
||||||
|
./media_ctrl_cli set-link /dev/media0 1:0 2:0 1
|
||||||
|
|
||||||
|
# Disable a link
|
||||||
|
./media_ctrl_cli set-link /dev/media0 1:0 2:0 0
|
||||||
|
```
|
||||||
|
|
||||||
|
Immutable links cannot be changed and will return an error.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Relationship to the Video Routing System
|
||||||
|
|
||||||
|
`media_ctrl_cli` exercises the `media_ctrl` module, which is used in the video routing system for:
|
||||||
|
|
||||||
|
- **Remote device enumeration** — a connecting node can query the media topology of a remote host to discover available cameras before opening any streams
|
||||||
|
- **Pipeline configuration** — setting pad formats and enabling links is required before a V4L2 capture node can be used, particularly on platforms like the Raspberry Pi where the sensor and capture node are separate media entities
|
||||||
|
|
||||||
|
See also: [`v4l2_ctrl_cli.md`](v4l2_ctrl_cli.md) for runtime camera parameter control.
|
||||||
147
docs/cli/v4l2_ctrl_cli.md
Normal file
147
docs/cli/v4l2_ctrl_cli.md
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
# v4l2_ctrl_cli
|
||||||
|
|
||||||
|
A development tool for listing and adjusting V4L2 camera controls. It wraps the `v4l2_ctrl` module and provides a command-line interface for enumerating, reading, and writing camera parameters on `/dev/video*` devices.
|
||||||
|
|
||||||
|
This tool is the counterpart to `v4l2-ctl` from the `v4l-utils` package. It covers the same core functionality but is built on our own `v4l2_ctrl` translation unit.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Build
|
||||||
|
|
||||||
|
From the repository root:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
make
|
||||||
|
```
|
||||||
|
|
||||||
|
Or from `dev/cli/` directly:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
make v4l2_ctrl_cli
|
||||||
|
```
|
||||||
|
|
||||||
|
The binary is placed in `dev/cli/`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
### `list`
|
||||||
|
|
||||||
|
Enumerate all video devices present on the system.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
./v4l2_ctrl_cli list
|
||||||
|
```
|
||||||
|
|
||||||
|
Example output:
|
||||||
|
|
||||||
|
```
|
||||||
|
Video devices:
|
||||||
|
/dev/video0
|
||||||
|
/dev/video1
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `controls <device>`
|
||||||
|
|
||||||
|
List all controls available on a device, including their current values, valid ranges, and — for menu controls — all available menu options.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
./v4l2_ctrl_cli controls /dev/video0
|
||||||
|
```
|
||||||
|
|
||||||
|
Example output (Pi camera):
|
||||||
|
|
||||||
|
```
|
||||||
|
Controls on /dev/video0:
|
||||||
|
0x009e0903 Exposure Time (Absolute) int current=10000 default=10000 min=13 max=11766488 step=1
|
||||||
|
0x009e0902 Analogue Gain int current=1000 default=1000 min=1000 max=16000 step=1
|
||||||
|
0x00980913 Horizontal Flip bool current=0 default=0 min=0 max=1
|
||||||
|
0x00980914 Vertical Flip bool current=0 default=0 min=0 max=1
|
||||||
|
0x009e0905 Auto Exposure menu current=0 default=0 min=0 max=1
|
||||||
|
[0] Manual Mode
|
||||||
|
[1] Auto Mode
|
||||||
|
0x0098091f Auto White Balance bool current=1 default=1 min=0 max=1
|
||||||
|
```
|
||||||
|
|
||||||
|
Control types:
|
||||||
|
- `int` — integer value with min, max, and step
|
||||||
|
- `bool` — 0 or 1
|
||||||
|
- `menu` — discrete named options (shown with their labels)
|
||||||
|
- `int-menu` — discrete integer-valued options
|
||||||
|
- `int64` — 64-bit integer
|
||||||
|
- `bitmask` — bit field
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `get <device> <id>`
|
||||||
|
|
||||||
|
Read the current value of a single control by its numeric ID. The ID can be decimal or hexadecimal (with `0x` prefix).
|
||||||
|
|
||||||
|
```sh
|
||||||
|
./v4l2_ctrl_cli get /dev/video0 0x009e0903
|
||||||
|
```
|
||||||
|
|
||||||
|
Example output:
|
||||||
|
|
||||||
|
```
|
||||||
|
0x009e0903 = 10000
|
||||||
|
```
|
||||||
|
|
||||||
|
Control IDs are shown in hex in the `controls` output.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `set <device> <id>=<value>`
|
||||||
|
|
||||||
|
Write a value to a control. The ID and value can both be decimal or hexadecimal.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# Set exposure time
|
||||||
|
./v4l2_ctrl_cli set /dev/video0 0x009e0903=5000
|
||||||
|
|
||||||
|
# Enable horizontal flip
|
||||||
|
./v4l2_ctrl_cli set /dev/video0 0x00980913=1
|
||||||
|
|
||||||
|
# Disable auto exposure (switch to manual)
|
||||||
|
./v4l2_ctrl_cli set /dev/video0 0x009e0905=0
|
||||||
|
```
|
||||||
|
|
||||||
|
The device will return an error if:
|
||||||
|
- The value is out of the control's min/max range
|
||||||
|
- The control is read-only
|
||||||
|
- The control does not exist on the device
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Common Control IDs
|
||||||
|
|
||||||
|
These IDs are standardised in the V4L2 specification and are consistent across devices that support them:
|
||||||
|
|
||||||
|
| ID | Name |
|
||||||
|
|---|---|
|
||||||
|
| `0x00980900` | Brightness |
|
||||||
|
| `0x00980901` | Contrast |
|
||||||
|
| `0x00980902` | Saturation |
|
||||||
|
| `0x00980913` | Horizontal Flip |
|
||||||
|
| `0x00980914` | Vertical Flip |
|
||||||
|
| `0x009e0903` | Exposure Time (Absolute) |
|
||||||
|
| `0x009e0902` | Analogue Gain |
|
||||||
|
| `0x009e0905` | Auto Exposure |
|
||||||
|
|
||||||
|
Camera-specific controls (ISP parameters, codec settings, etc.) will have IDs outside the standard ranges and are best discovered via the `controls` command.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Relationship to the Video Routing System
|
||||||
|
|
||||||
|
`v4l2_ctrl_cli` exercises the `v4l2_ctrl` module, which is used in the video routing system for:
|
||||||
|
|
||||||
|
- **Remote parameter tuning** — a controlling node can adjust exposure, gain, white balance, and other parameters on a remote camera without SSH or local access
|
||||||
|
- **Control enumeration** — discovering the full set of parameters a camera supports, returned as structured data over the transport control channel
|
||||||
|
|
||||||
|
The Pi in the microscope setup runs a V4L2 control endpoint that accepts these operations via the transport protocol. `v4l2_ctrl_cli` lets you perform the same operations locally for development and calibration.
|
||||||
|
|
||||||
|
See also: [`media_ctrl_cli.md`](media_ctrl_cli.md) for pipeline topology and pad format configuration.
|
||||||
59
include/error.h
Normal file
59
include/error.h
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <errno.h>
|
||||||
|
|
||||||
|
typedef enum Error_Code {
|
||||||
|
ERR_NONE = 0,
|
||||||
|
ERR_SYSCALL = 1,
|
||||||
|
ERR_INVALID = 2,
|
||||||
|
ERR_NOT_FOUND = 3,
|
||||||
|
} Error_Code;
|
||||||
|
|
||||||
|
struct Syscall_Error_Detail {
|
||||||
|
int err_no;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Invalid_Error_Detail {
|
||||||
|
/* fields added as concrete cases arise */
|
||||||
|
int placeholder;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct App_Error {
|
||||||
|
Error_Code code;
|
||||||
|
const char *file;
|
||||||
|
int line;
|
||||||
|
union {
|
||||||
|
struct Syscall_Error_Detail syscall;
|
||||||
|
struct Invalid_Error_Detail invalid;
|
||||||
|
} detail;
|
||||||
|
};
|
||||||
|
|
||||||
|
void app_error_print(struct App_Error *e);
|
||||||
|
|
||||||
|
#define APP_OK \
|
||||||
|
((struct App_Error){ .code = ERR_NONE })
|
||||||
|
|
||||||
|
#define APP_IS_OK(e) \
|
||||||
|
((e).code == ERR_NONE)
|
||||||
|
|
||||||
|
#define APP_SYSCALL_ERROR() \
|
||||||
|
((struct App_Error){ \
|
||||||
|
.code = ERR_SYSCALL, \
|
||||||
|
.file = __FILE__, \
|
||||||
|
.line = __LINE__, \
|
||||||
|
.detail = { .syscall = { .err_no = errno } }, \
|
||||||
|
})
|
||||||
|
|
||||||
|
#define APP_INVALID_ERROR() \
|
||||||
|
((struct App_Error){ \
|
||||||
|
.code = ERR_INVALID, \
|
||||||
|
.file = __FILE__, \
|
||||||
|
.line = __LINE__, \
|
||||||
|
})
|
||||||
|
|
||||||
|
#define APP_NOT_FOUND_ERROR() \
|
||||||
|
((struct App_Error){ \
|
||||||
|
.code = ERR_NOT_FOUND, \
|
||||||
|
.file = __FILE__, \
|
||||||
|
.line = __LINE__, \
|
||||||
|
})
|
||||||
90
include/media_ctrl.h
Normal file
90
include/media_ctrl.h
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
#include "error.h"
|
||||||
|
|
||||||
|
/* Opaque handle — defined in media_ctrl.c */
|
||||||
|
struct Media_Ctrl;
|
||||||
|
|
||||||
|
struct Media_Device_Info {
|
||||||
|
char driver[16];
|
||||||
|
char model[32];
|
||||||
|
char serial[40];
|
||||||
|
char bus_info[32];
|
||||||
|
uint32_t media_version;
|
||||||
|
uint32_t hw_revision;
|
||||||
|
uint32_t driver_version;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Media_Entity {
|
||||||
|
uint32_t id;
|
||||||
|
char name[32];
|
||||||
|
uint32_t type; /* MEDIA_ENT_F_* value from kernel */
|
||||||
|
uint32_t flags;
|
||||||
|
uint16_t pad_count;
|
||||||
|
uint16_t link_count;
|
||||||
|
uint32_t dev_major; /* associated device node major, 0 if none */
|
||||||
|
uint32_t dev_minor;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Media_Pad {
|
||||||
|
uint32_t entity_id;
|
||||||
|
uint16_t index;
|
||||||
|
uint32_t flags; /* MEDIA_PAD_FL_SINK / MEDIA_PAD_FL_SOURCE */
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Media_Link {
|
||||||
|
struct Media_Pad source;
|
||||||
|
struct Media_Pad sink;
|
||||||
|
uint32_t flags; /* MEDIA_LNK_FL_ENABLED, MEDIA_LNK_FL_IMMUTABLE */
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Enumerate all /dev/media* device nodes present on the system.
|
||||||
|
* Calls callback once per device path found.
|
||||||
|
*/
|
||||||
|
struct App_Error media_ctrl_find_devices(
|
||||||
|
void (*callback)(const char *device_path, void *userdata),
|
||||||
|
void *userdata);
|
||||||
|
|
||||||
|
struct App_Error media_ctrl_open(const char *device_path, struct Media_Ctrl **out);
|
||||||
|
void media_ctrl_close(struct Media_Ctrl *ctrl);
|
||||||
|
|
||||||
|
struct App_Error media_ctrl_get_info(
|
||||||
|
struct Media_Ctrl *ctrl,
|
||||||
|
struct Media_Device_Info *out);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Enumerate all entities in the media graph.
|
||||||
|
*/
|
||||||
|
struct App_Error media_ctrl_enum_entities(
|
||||||
|
struct Media_Ctrl *ctrl,
|
||||||
|
void (*callback)(const struct Media_Entity *entity, void *userdata),
|
||||||
|
void *userdata);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Enumerate pads and links for a specific entity.
|
||||||
|
* Pass the entity struct obtained from media_ctrl_enum_entities —
|
||||||
|
* the pad and link counts are taken from it.
|
||||||
|
* Either callback may be NULL if that information is not needed.
|
||||||
|
*/
|
||||||
|
struct App_Error media_ctrl_enum_entity_pads_and_links(
|
||||||
|
struct Media_Ctrl *ctrl,
|
||||||
|
const struct Media_Entity *entity,
|
||||||
|
void (*pad_callback)(const struct Media_Pad *pad, void *userdata),
|
||||||
|
void (*link_callback)(const struct Media_Link *link, void *userdata),
|
||||||
|
void *userdata);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Enable or disable a link between two pads.
|
||||||
|
* Immutable links cannot be changed and will return an error.
|
||||||
|
*/
|
||||||
|
struct App_Error media_ctrl_set_link(
|
||||||
|
struct Media_Ctrl *ctrl,
|
||||||
|
uint32_t source_entity_id, uint16_t source_pad_index,
|
||||||
|
uint32_t sink_entity_id, uint16_t sink_pad_index,
|
||||||
|
int enabled);
|
||||||
|
|
||||||
|
/* Human-readable names for display */
|
||||||
|
const char *media_entity_type_name(uint32_t type);
|
||||||
|
const char *media_pad_flag_name(uint32_t flags);
|
||||||
75
include/v4l2_ctrl.h
Normal file
75
include/v4l2_ctrl.h
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
#include "error.h"
|
||||||
|
|
||||||
|
/* Opaque handle — defined in v4l2_ctrl.c */
|
||||||
|
struct V4l2_Ctrl_Handle;
|
||||||
|
|
||||||
|
typedef enum V4l2_Ctrl_Type {
|
||||||
|
CTRL_TYPE_INTEGER = 1,
|
||||||
|
CTRL_TYPE_BOOLEAN = 2,
|
||||||
|
CTRL_TYPE_MENU = 3,
|
||||||
|
CTRL_TYPE_BUTTON = 4,
|
||||||
|
CTRL_TYPE_INTEGER64 = 5,
|
||||||
|
CTRL_TYPE_CTRL_CLASS = 6,
|
||||||
|
CTRL_TYPE_STRING = 7,
|
||||||
|
CTRL_TYPE_BITMASK = 8,
|
||||||
|
CTRL_TYPE_INTEGER_MENU = 9,
|
||||||
|
CTRL_TYPE_UNKNOWN = 0xff,
|
||||||
|
} V4l2_Ctrl_Type;
|
||||||
|
|
||||||
|
struct V4l2_Ctrl_Desc {
|
||||||
|
uint32_t id;
|
||||||
|
char name[32];
|
||||||
|
V4l2_Ctrl_Type type;
|
||||||
|
int32_t min;
|
||||||
|
int32_t max;
|
||||||
|
int32_t step;
|
||||||
|
int32_t default_value;
|
||||||
|
int32_t current_value;
|
||||||
|
uint32_t flags;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct V4l2_Menu_Item {
|
||||||
|
uint32_t index;
|
||||||
|
char name[32]; /* for CTRL_TYPE_MENU */
|
||||||
|
int64_t value; /* for CTRL_TYPE_INTEGER_MENU */
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Enumerate all /dev/video* device nodes present on the system.
|
||||||
|
*/
|
||||||
|
struct App_Error v4l2_ctrl_find_devices(
|
||||||
|
void (*callback)(const char *device_path, void *userdata),
|
||||||
|
void *userdata);
|
||||||
|
|
||||||
|
struct App_Error v4l2_ctrl_open(const char *device_path, struct V4l2_Ctrl_Handle **out);
|
||||||
|
void v4l2_ctrl_close(struct V4l2_Ctrl_Handle *handle);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Enumerate all controls on the device.
|
||||||
|
* For menu and integer-menu controls, menu_items is a non-NULL array of
|
||||||
|
* menu_count items. For all other types, menu_items is NULL and menu_count is 0.
|
||||||
|
*/
|
||||||
|
struct App_Error v4l2_ctrl_enumerate(
|
||||||
|
struct V4l2_Ctrl_Handle *handle,
|
||||||
|
void (*callback)(
|
||||||
|
const struct V4l2_Ctrl_Desc *desc,
|
||||||
|
uint32_t menu_count,
|
||||||
|
const struct V4l2_Menu_Item *menu_items,
|
||||||
|
void *userdata),
|
||||||
|
void *userdata);
|
||||||
|
|
||||||
|
struct App_Error v4l2_ctrl_get(
|
||||||
|
struct V4l2_Ctrl_Handle *handle,
|
||||||
|
uint32_t id,
|
||||||
|
int32_t *value_out);
|
||||||
|
|
||||||
|
struct App_Error v4l2_ctrl_set(
|
||||||
|
struct V4l2_Ctrl_Handle *handle,
|
||||||
|
uint32_t id,
|
||||||
|
int32_t value);
|
||||||
|
|
||||||
|
/* Human-readable name for a control type */
|
||||||
|
const char *v4l2_ctrl_type_name(V4l2_Ctrl_Type type);
|
||||||
17
src/modules/common/Makefile
Normal file
17
src/modules/common/Makefile
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
ROOT := $(abspath ../../..)
|
||||||
|
CC = gcc
|
||||||
|
CFLAGS = -std=c11 -Wall -Wextra -D_GNU_SOURCE -I$(ROOT)/include
|
||||||
|
BUILD = $(ROOT)/build/common
|
||||||
|
|
||||||
|
.PHONY: all clean
|
||||||
|
|
||||||
|
all: $(BUILD)/error.o
|
||||||
|
|
||||||
|
$(BUILD)/error.o: error.c $(ROOT)/include/error.h | $(BUILD)
|
||||||
|
$(CC) $(CFLAGS) -c -o $@ $<
|
||||||
|
|
||||||
|
$(BUILD):
|
||||||
|
mkdir -p $@
|
||||||
|
|
||||||
|
clean:
|
||||||
|
rm -f $(BUILD)/error.o
|
||||||
29
src/modules/common/error.c
Normal file
29
src/modules/common/error.c
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
#include <stdio.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
#include "error.h"
|
||||||
|
|
||||||
|
void app_error_print(struct App_Error *e) {
|
||||||
|
if (APP_IS_OK(*e)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
fprintf(stderr, "%s:%d: ", e->file, e->line);
|
||||||
|
|
||||||
|
switch (e->code) {
|
||||||
|
case ERR_NONE:
|
||||||
|
break;
|
||||||
|
case ERR_SYSCALL:
|
||||||
|
fprintf(stderr, "syscall error: %s\n", strerror(e->detail.syscall.err_no));
|
||||||
|
break;
|
||||||
|
case ERR_INVALID:
|
||||||
|
fprintf(stderr, "invalid argument\n");
|
||||||
|
break;
|
||||||
|
case ERR_NOT_FOUND:
|
||||||
|
fprintf(stderr, "not found\n");
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
fprintf(stderr, "unknown error (code %d)\n", (int)e->code);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
17
src/modules/media_ctrl/Makefile
Normal file
17
src/modules/media_ctrl/Makefile
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
ROOT := $(abspath ../../..)
|
||||||
|
CC = gcc
|
||||||
|
CFLAGS = -std=c11 -Wall -Wextra -D_GNU_SOURCE -I$(ROOT)/include
|
||||||
|
BUILD = $(ROOT)/build/media_ctrl
|
||||||
|
|
||||||
|
.PHONY: all clean
|
||||||
|
|
||||||
|
all: $(BUILD)/media_ctrl.o
|
||||||
|
|
||||||
|
$(BUILD)/media_ctrl.o: media_ctrl.c $(ROOT)/include/media_ctrl.h $(ROOT)/include/error.h | $(BUILD)
|
||||||
|
$(CC) $(CFLAGS) -c -o $@ $<
|
||||||
|
|
||||||
|
$(BUILD):
|
||||||
|
mkdir -p $@
|
||||||
|
|
||||||
|
clean:
|
||||||
|
rm -f $(BUILD)/media_ctrl.o
|
||||||
283
src/modules/media_ctrl/media_ctrl.c
Normal file
283
src/modules/media_ctrl/media_ctrl.c
Normal file
@@ -0,0 +1,283 @@
|
|||||||
|
#include <stdlib.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <fcntl.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
#include <errno.h>
|
||||||
|
#include <glob.h>
|
||||||
|
#include <sys/ioctl.h>
|
||||||
|
#include <linux/media.h>
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Kernel compatibility checks.
|
||||||
|
* The media entity type constants were renamed between kernel versions.
|
||||||
|
* Older kernels use MEDIA_ENT_T_* under MEDIA_ENT_TYPE_MASK.
|
||||||
|
* Newer kernels use MEDIA_ENT_F_* (function-based naming).
|
||||||
|
* We support the older API here; warn clearly if the expected symbols are absent.
|
||||||
|
*/
|
||||||
|
#ifndef MEDIA_ENT_ID_FLAG_NEXT
|
||||||
|
# warning "MEDIA_ENT_ID_FLAG_NEXT not defined — entity enumeration will not work"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifndef MEDIA_ENT_TYPE_MASK
|
||||||
|
# warning "MEDIA_ENT_TYPE_MASK not defined — entity type classification will not work"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifndef MEDIA_ENT_T_DEVNODE
|
||||||
|
# warning "MEDIA_ENT_T_DEVNODE not defined — devnode entity type will not be recognised"
|
||||||
|
# define MEDIA_ENT_T_DEVNODE 0x00000002
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifndef MEDIA_ENT_T_V4L2_SUBDEV
|
||||||
|
# warning "MEDIA_ENT_T_V4L2_SUBDEV not defined — subdev entity type will not be recognised"
|
||||||
|
# define MEDIA_ENT_T_V4L2_SUBDEV 0x00020000
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifndef MEDIA_LNK_FL_ENABLED
|
||||||
|
# warning "MEDIA_LNK_FL_ENABLED not defined"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifndef MEDIA_LNK_FL_IMMUTABLE
|
||||||
|
# warning "MEDIA_LNK_FL_IMMUTABLE not defined"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifndef MEDIA_PAD_FL_SOURCE
|
||||||
|
# warning "MEDIA_PAD_FL_SOURCE not defined"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifndef MEDIA_PAD_FL_SINK
|
||||||
|
# warning "MEDIA_PAD_FL_SINK not defined"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#include "error.h"
|
||||||
|
#include "media_ctrl.h"
|
||||||
|
|
||||||
|
struct Media_Ctrl {
|
||||||
|
int fd;
|
||||||
|
char device_path[256];
|
||||||
|
};
|
||||||
|
|
||||||
|
struct App_Error media_ctrl_find_devices(
|
||||||
|
void (*callback)(const char *device_path, void *userdata),
|
||||||
|
void *userdata)
|
||||||
|
{
|
||||||
|
glob_t g;
|
||||||
|
|
||||||
|
int r = glob("/dev/media*", 0, NULL, &g);
|
||||||
|
if (r == GLOB_NOMATCH) {
|
||||||
|
globfree(&g);
|
||||||
|
return APP_OK;
|
||||||
|
}
|
||||||
|
if (r != 0) {
|
||||||
|
return APP_SYSCALL_ERROR();
|
||||||
|
}
|
||||||
|
|
||||||
|
for (size_t i = 0; i < g.gl_pathc; i++) {
|
||||||
|
callback(g.gl_pathv[i], userdata);
|
||||||
|
}
|
||||||
|
|
||||||
|
globfree(&g);
|
||||||
|
return APP_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct App_Error media_ctrl_open(const char *device_path, struct Media_Ctrl **out) {
|
||||||
|
int fd = open(device_path, O_RDWR | O_CLOEXEC);
|
||||||
|
if (fd < 0) {
|
||||||
|
return APP_SYSCALL_ERROR();
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Media_Ctrl *ctrl = malloc(sizeof(struct Media_Ctrl));
|
||||||
|
if (!ctrl) {
|
||||||
|
close(fd);
|
||||||
|
return APP_SYSCALL_ERROR();
|
||||||
|
}
|
||||||
|
|
||||||
|
ctrl->fd = fd;
|
||||||
|
strncpy(ctrl->device_path, device_path, sizeof(ctrl->device_path) - 1);
|
||||||
|
ctrl->device_path[sizeof(ctrl->device_path) - 1] = '\0';
|
||||||
|
|
||||||
|
*out = ctrl;
|
||||||
|
return APP_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
void media_ctrl_close(struct Media_Ctrl *ctrl) {
|
||||||
|
if (!ctrl) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
close(ctrl->fd);
|
||||||
|
free(ctrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
struct App_Error media_ctrl_get_info(
|
||||||
|
struct Media_Ctrl *ctrl,
|
||||||
|
struct Media_Device_Info *out)
|
||||||
|
{
|
||||||
|
struct media_device_info info;
|
||||||
|
memset(&info, 0, sizeof(info));
|
||||||
|
|
||||||
|
if (ioctl(ctrl->fd, MEDIA_IOC_DEVICE_INFO, &info) < 0) {
|
||||||
|
return APP_SYSCALL_ERROR();
|
||||||
|
}
|
||||||
|
|
||||||
|
memcpy(out->driver, info.driver, sizeof(out->driver));
|
||||||
|
memcpy(out->model, info.model, sizeof(out->model));
|
||||||
|
memcpy(out->serial, info.serial, sizeof(out->serial));
|
||||||
|
memcpy(out->bus_info, info.bus_info, sizeof(out->bus_info));
|
||||||
|
out->media_version = info.media_version;
|
||||||
|
out->hw_revision = info.hw_revision;
|
||||||
|
out->driver_version = info.driver_version;
|
||||||
|
|
||||||
|
return APP_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct App_Error media_ctrl_enum_entities(
|
||||||
|
struct Media_Ctrl *ctrl,
|
||||||
|
void (*callback)(const struct Media_Entity *entity, void *userdata),
|
||||||
|
void *userdata)
|
||||||
|
{
|
||||||
|
struct media_entity_desc desc;
|
||||||
|
|
||||||
|
memset(&desc, 0, sizeof(desc));
|
||||||
|
desc.id = MEDIA_ENT_ID_FLAG_NEXT;
|
||||||
|
|
||||||
|
while (ioctl(ctrl->fd, MEDIA_IOC_ENUM_ENTITIES, &desc) == 0) {
|
||||||
|
struct Media_Entity entity;
|
||||||
|
memset(&entity, 0, sizeof(entity));
|
||||||
|
|
||||||
|
entity.id = desc.id;
|
||||||
|
entity.type = desc.type;
|
||||||
|
entity.flags = desc.flags;
|
||||||
|
entity.pad_count = desc.pads;
|
||||||
|
entity.link_count = desc.links;
|
||||||
|
strncpy(entity.name, desc.name, sizeof(entity.name) - 1);
|
||||||
|
|
||||||
|
/* Extract device node info when available */
|
||||||
|
if (desc.type == MEDIA_ENT_T_DEVNODE ||
|
||||||
|
(desc.type & MEDIA_ENT_TYPE_MASK) == MEDIA_ENT_T_DEVNODE)
|
||||||
|
{
|
||||||
|
entity.dev_major = desc.v4l.major;
|
||||||
|
entity.dev_minor = desc.v4l.minor;
|
||||||
|
} else {
|
||||||
|
entity.dev_major = 0;
|
||||||
|
entity.dev_minor = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
callback(&entity, userdata);
|
||||||
|
|
||||||
|
desc.id |= MEDIA_ENT_ID_FLAG_NEXT;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (errno != EINVAL) {
|
||||||
|
return APP_SYSCALL_ERROR();
|
||||||
|
}
|
||||||
|
|
||||||
|
return APP_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct App_Error media_ctrl_enum_entity_pads_and_links(
|
||||||
|
struct Media_Ctrl *ctrl,
|
||||||
|
const struct Media_Entity *entity,
|
||||||
|
void (*pad_callback)(const struct Media_Pad *pad, void *userdata),
|
||||||
|
void (*link_callback)(const struct Media_Link *link, void *userdata),
|
||||||
|
void *userdata)
|
||||||
|
{
|
||||||
|
struct media_links_enum links_enum;
|
||||||
|
struct media_pad_desc *pads = NULL;
|
||||||
|
struct media_link_desc *links = NULL;
|
||||||
|
struct App_Error err = APP_OK;
|
||||||
|
|
||||||
|
memset(&links_enum, 0, sizeof(links_enum));
|
||||||
|
links_enum.entity = entity->id;
|
||||||
|
|
||||||
|
if (entity->pad_count > 0) {
|
||||||
|
pads = calloc(entity->pad_count, sizeof(struct media_pad_desc));
|
||||||
|
if (!pads) {
|
||||||
|
return APP_SYSCALL_ERROR();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entity->link_count > 0) {
|
||||||
|
links = calloc(entity->link_count, sizeof(struct media_link_desc));
|
||||||
|
if (!links) {
|
||||||
|
free(pads);
|
||||||
|
return APP_SYSCALL_ERROR();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
links_enum.pads = pads;
|
||||||
|
links_enum.links = links;
|
||||||
|
|
||||||
|
if (ioctl(ctrl->fd, MEDIA_IOC_ENUM_LINKS, &links_enum) < 0) {
|
||||||
|
err = APP_SYSCALL_ERROR();
|
||||||
|
goto cleanup;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pad_callback) {
|
||||||
|
for (uint16_t i = 0; i < entity->pad_count; i++) {
|
||||||
|
struct Media_Pad pad;
|
||||||
|
pad.entity_id = pads[i].entity;
|
||||||
|
pad.index = pads[i].index;
|
||||||
|
pad.flags = pads[i].flags;
|
||||||
|
pad_callback(&pad, userdata);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (link_callback) {
|
||||||
|
for (uint16_t i = 0; i < entity->link_count; i++) {
|
||||||
|
struct Media_Link link;
|
||||||
|
link.source.entity_id = links[i].source.entity;
|
||||||
|
link.source.index = links[i].source.index;
|
||||||
|
link.source.flags = links[i].source.flags;
|
||||||
|
link.sink.entity_id = links[i].sink.entity;
|
||||||
|
link.sink.index = links[i].sink.index;
|
||||||
|
link.sink.flags = links[i].sink.flags;
|
||||||
|
link.flags = links[i].flags;
|
||||||
|
link_callback(&link, userdata);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanup:
|
||||||
|
free(pads);
|
||||||
|
free(links);
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct App_Error media_ctrl_set_link(
|
||||||
|
struct Media_Ctrl *ctrl,
|
||||||
|
uint32_t source_entity_id, uint16_t source_pad_index,
|
||||||
|
uint32_t sink_entity_id, uint16_t sink_pad_index,
|
||||||
|
int enabled)
|
||||||
|
{
|
||||||
|
struct media_link_desc desc;
|
||||||
|
memset(&desc, 0, sizeof(desc));
|
||||||
|
|
||||||
|
desc.source.entity = source_entity_id;
|
||||||
|
desc.source.index = source_pad_index;
|
||||||
|
desc.sink.entity = sink_entity_id;
|
||||||
|
desc.sink.index = sink_pad_index;
|
||||||
|
desc.flags = enabled ? MEDIA_LNK_FL_ENABLED : 0;
|
||||||
|
|
||||||
|
if (ioctl(ctrl->fd, MEDIA_IOC_SETUP_LINK, &desc) < 0) {
|
||||||
|
return APP_SYSCALL_ERROR();
|
||||||
|
}
|
||||||
|
|
||||||
|
return APP_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
const char *media_entity_type_name(uint32_t type) {
|
||||||
|
switch (type & MEDIA_ENT_TYPE_MASK) {
|
||||||
|
case MEDIA_ENT_T_DEVNODE: return "devnode";
|
||||||
|
case MEDIA_ENT_T_V4L2_SUBDEV: return "v4l2-subdev";
|
||||||
|
default: return "unknown";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const char *media_pad_flag_name(uint32_t flags) {
|
||||||
|
if (flags & MEDIA_PAD_FL_SOURCE) {
|
||||||
|
return "source";
|
||||||
|
}
|
||||||
|
if (flags & MEDIA_PAD_FL_SINK) {
|
||||||
|
return "sink";
|
||||||
|
}
|
||||||
|
return "unknown";
|
||||||
|
}
|
||||||
17
src/modules/v4l2_ctrl/Makefile
Normal file
17
src/modules/v4l2_ctrl/Makefile
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
ROOT := $(abspath ../../..)
|
||||||
|
CC = gcc
|
||||||
|
CFLAGS = -std=c11 -Wall -Wextra -D_GNU_SOURCE -I$(ROOT)/include
|
||||||
|
BUILD = $(ROOT)/build/v4l2_ctrl
|
||||||
|
|
||||||
|
.PHONY: all clean
|
||||||
|
|
||||||
|
all: $(BUILD)/v4l2_ctrl.o
|
||||||
|
|
||||||
|
$(BUILD)/v4l2_ctrl.o: v4l2_ctrl.c $(ROOT)/include/v4l2_ctrl.h $(ROOT)/include/error.h | $(BUILD)
|
||||||
|
$(CC) $(CFLAGS) -c -o $@ $<
|
||||||
|
|
||||||
|
$(BUILD):
|
||||||
|
mkdir -p $@
|
||||||
|
|
||||||
|
clean:
|
||||||
|
rm -f $(BUILD)/v4l2_ctrl.o
|
||||||
292
src/modules/v4l2_ctrl/v4l2_ctrl.c
Normal file
292
src/modules/v4l2_ctrl/v4l2_ctrl.c
Normal file
@@ -0,0 +1,292 @@
|
|||||||
|
#include <stdlib.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <fcntl.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
#include <errno.h>
|
||||||
|
#include <glob.h>
|
||||||
|
#include <sys/ioctl.h>
|
||||||
|
#include <linux/videodev2.h>
|
||||||
|
|
||||||
|
/* Kernel compatibility checks. */
|
||||||
|
#ifndef VIDIOC_QUERY_EXT_CTRL
|
||||||
|
# warning "VIDIOC_QUERY_EXT_CTRL not defined — requires kernel >= 3.6; control enumeration will not work"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifndef V4L2_CTRL_FLAG_NEXT_CTRL
|
||||||
|
# warning "V4L2_CTRL_FLAG_NEXT_CTRL not defined"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifndef V4L2_CTRL_FLAG_NEXT_COMPOUND
|
||||||
|
# warning "V4L2_CTRL_FLAG_NEXT_COMPOUND not defined"
|
||||||
|
# define V4L2_CTRL_FLAG_NEXT_COMPOUND 0
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifndef V4L2_CTRL_WHICH_CUR_VAL
|
||||||
|
# warning "V4L2_CTRL_WHICH_CUR_VAL not defined"
|
||||||
|
# define V4L2_CTRL_WHICH_CUR_VAL 0
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#include "error.h"
|
||||||
|
#include "v4l2_ctrl.h"
|
||||||
|
|
||||||
|
struct V4l2_Ctrl_Handle {
|
||||||
|
int fd;
|
||||||
|
char device_path[256];
|
||||||
|
};
|
||||||
|
|
||||||
|
struct App_Error v4l2_ctrl_find_devices(
|
||||||
|
void (*callback)(const char *device_path, void *userdata),
|
||||||
|
void *userdata)
|
||||||
|
{
|
||||||
|
glob_t g;
|
||||||
|
|
||||||
|
int r = glob("/dev/video*", 0, NULL, &g);
|
||||||
|
if (r == GLOB_NOMATCH) {
|
||||||
|
globfree(&g);
|
||||||
|
return APP_OK;
|
||||||
|
}
|
||||||
|
if (r != 0) {
|
||||||
|
return APP_SYSCALL_ERROR();
|
||||||
|
}
|
||||||
|
|
||||||
|
for (size_t i = 0; i < g.gl_pathc; i++) {
|
||||||
|
callback(g.gl_pathv[i], userdata);
|
||||||
|
}
|
||||||
|
|
||||||
|
globfree(&g);
|
||||||
|
return APP_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct App_Error v4l2_ctrl_open(const char *device_path, struct V4l2_Ctrl_Handle **out) {
|
||||||
|
int fd = open(device_path, O_RDWR | O_CLOEXEC);
|
||||||
|
if (fd < 0) {
|
||||||
|
return APP_SYSCALL_ERROR();
|
||||||
|
}
|
||||||
|
|
||||||
|
struct V4l2_Ctrl_Handle *handle = malloc(sizeof(struct V4l2_Ctrl_Handle));
|
||||||
|
if (!handle) {
|
||||||
|
close(fd);
|
||||||
|
return APP_SYSCALL_ERROR();
|
||||||
|
}
|
||||||
|
|
||||||
|
handle->fd = fd;
|
||||||
|
strncpy(handle->device_path, device_path, sizeof(handle->device_path) - 1);
|
||||||
|
handle->device_path[sizeof(handle->device_path) - 1] = '\0';
|
||||||
|
|
||||||
|
*out = handle;
|
||||||
|
return APP_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
void v4l2_ctrl_close(struct V4l2_Ctrl_Handle *handle) {
|
||||||
|
if (!handle) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
close(handle->fd);
|
||||||
|
free(handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
static V4l2_Ctrl_Type map_ctrl_type(uint32_t kernel_type) {
|
||||||
|
switch (kernel_type) {
|
||||||
|
case V4L2_CTRL_TYPE_INTEGER: return CTRL_TYPE_INTEGER;
|
||||||
|
case V4L2_CTRL_TYPE_BOOLEAN: return CTRL_TYPE_BOOLEAN;
|
||||||
|
case V4L2_CTRL_TYPE_MENU: return CTRL_TYPE_MENU;
|
||||||
|
case V4L2_CTRL_TYPE_BUTTON: return CTRL_TYPE_BUTTON;
|
||||||
|
case V4L2_CTRL_TYPE_INTEGER64: return CTRL_TYPE_INTEGER64;
|
||||||
|
case V4L2_CTRL_TYPE_CTRL_CLASS: return CTRL_TYPE_CTRL_CLASS;
|
||||||
|
case V4L2_CTRL_TYPE_STRING: return CTRL_TYPE_STRING;
|
||||||
|
case V4L2_CTRL_TYPE_BITMASK: return CTRL_TYPE_BITMASK;
|
||||||
|
case V4L2_CTRL_TYPE_INTEGER_MENU: return CTRL_TYPE_INTEGER_MENU;
|
||||||
|
default: return CTRL_TYPE_UNKNOWN;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Fetch menu items for a menu or integer-menu control.
|
||||||
|
* Returns a malloc'd array and writes the count to count_out.
|
||||||
|
* Caller must free the returned array.
|
||||||
|
*/
|
||||||
|
static struct V4l2_Menu_Item *fetch_menu_items(
|
||||||
|
int fd,
|
||||||
|
const struct v4l2_query_ext_ctrl *qc,
|
||||||
|
uint32_t *count_out)
|
||||||
|
{
|
||||||
|
uint32_t n = 0;
|
||||||
|
|
||||||
|
/* Count valid items first */
|
||||||
|
for (int64_t i = qc->minimum; i <= qc->maximum; i += qc->step > 0 ? qc->step : 1) {
|
||||||
|
struct v4l2_querymenu qm;
|
||||||
|
memset(&qm, 0, sizeof(qm));
|
||||||
|
qm.id = qc->id;
|
||||||
|
qm.index = (uint32_t)i;
|
||||||
|
if (ioctl(fd, VIDIOC_QUERYMENU, &qm) == 0) {
|
||||||
|
n++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (n == 0) {
|
||||||
|
*count_out = 0;
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct V4l2_Menu_Item *items = calloc(n, sizeof(struct V4l2_Menu_Item));
|
||||||
|
if (!items) {
|
||||||
|
*count_out = 0;
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t idx = 0;
|
||||||
|
for (int64_t i = qc->minimum; i <= qc->maximum; i += qc->step > 0 ? qc->step : 1) {
|
||||||
|
struct v4l2_querymenu qm;
|
||||||
|
memset(&qm, 0, sizeof(qm));
|
||||||
|
qm.id = qc->id;
|
||||||
|
qm.index = (uint32_t)i;
|
||||||
|
if (ioctl(fd, VIDIOC_QUERYMENU, &qm) == 0) {
|
||||||
|
items[idx].index = qm.index;
|
||||||
|
if (qc->type == V4L2_CTRL_TYPE_MENU) {
|
||||||
|
strncpy(items[idx].name, (char *)qm.name, sizeof(items[idx].name) - 1);
|
||||||
|
items[idx].value = (int64_t)qm.index;
|
||||||
|
} else {
|
||||||
|
items[idx].value = qm.value;
|
||||||
|
snprintf(items[idx].name, sizeof(items[idx].name), "%lld", (long long)qm.value);
|
||||||
|
}
|
||||||
|
idx++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
*count_out = idx;
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct App_Error v4l2_ctrl_enumerate(
|
||||||
|
struct V4l2_Ctrl_Handle *handle,
|
||||||
|
void (*callback)(
|
||||||
|
const struct V4l2_Ctrl_Desc *desc,
|
||||||
|
uint32_t menu_count,
|
||||||
|
const struct V4l2_Menu_Item *menu_items,
|
||||||
|
void *userdata),
|
||||||
|
void *userdata)
|
||||||
|
{
|
||||||
|
struct v4l2_query_ext_ctrl qc;
|
||||||
|
memset(&qc, 0, sizeof(qc));
|
||||||
|
qc.id = V4L2_CTRL_FLAG_NEXT_CTRL | V4L2_CTRL_FLAG_NEXT_COMPOUND;
|
||||||
|
|
||||||
|
while (ioctl(handle->fd, VIDIOC_QUERY_EXT_CTRL, &qc) == 0) {
|
||||||
|
/* Skip control class headers */
|
||||||
|
if (qc.type == V4L2_CTRL_TYPE_CTRL_CLASS) {
|
||||||
|
qc.id |= V4L2_CTRL_FLAG_NEXT_CTRL | V4L2_CTRL_FLAG_NEXT_COMPOUND;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct V4l2_Ctrl_Desc desc;
|
||||||
|
memset(&desc, 0, sizeof(desc));
|
||||||
|
|
||||||
|
desc.id = qc.id;
|
||||||
|
desc.type = map_ctrl_type(qc.type);
|
||||||
|
desc.min = (int32_t)qc.minimum;
|
||||||
|
desc.max = (int32_t)qc.maximum;
|
||||||
|
desc.step = (int32_t)qc.step;
|
||||||
|
desc.default_value = (int32_t)qc.default_value;
|
||||||
|
desc.flags = qc.flags;
|
||||||
|
strncpy(desc.name, qc.name, sizeof(desc.name) - 1);
|
||||||
|
|
||||||
|
/* Fetch current value */
|
||||||
|
struct v4l2_ext_control ctrl;
|
||||||
|
struct v4l2_ext_controls ctrls;
|
||||||
|
memset(&ctrl, 0, sizeof(ctrl));
|
||||||
|
memset(&ctrls, 0, sizeof(ctrls));
|
||||||
|
ctrl.id = qc.id;
|
||||||
|
ctrls.controls = &ctrl;
|
||||||
|
ctrls.count = 1;
|
||||||
|
ctrls.which = V4L2_CTRL_WHICH_CUR_VAL;
|
||||||
|
|
||||||
|
if (ioctl(handle->fd, VIDIOC_G_EXT_CTRLS, &ctrls) == 0) {
|
||||||
|
desc.current_value = ctrl.value;
|
||||||
|
} else {
|
||||||
|
desc.current_value = desc.default_value;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct V4l2_Menu_Item *menu_items = NULL;
|
||||||
|
uint32_t menu_count = 0;
|
||||||
|
|
||||||
|
if (qc.type == V4L2_CTRL_TYPE_MENU ||
|
||||||
|
qc.type == V4L2_CTRL_TYPE_INTEGER_MENU)
|
||||||
|
{
|
||||||
|
menu_items = fetch_menu_items(handle->fd, &qc, &menu_count);
|
||||||
|
}
|
||||||
|
|
||||||
|
callback(&desc, menu_count, menu_items, userdata);
|
||||||
|
|
||||||
|
free(menu_items);
|
||||||
|
|
||||||
|
qc.id |= V4L2_CTRL_FLAG_NEXT_CTRL | V4L2_CTRL_FLAG_NEXT_COMPOUND;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (errno != EINVAL) {
|
||||||
|
return APP_SYSCALL_ERROR();
|
||||||
|
}
|
||||||
|
|
||||||
|
return APP_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct App_Error v4l2_ctrl_get(
|
||||||
|
struct V4l2_Ctrl_Handle *handle,
|
||||||
|
uint32_t id,
|
||||||
|
int32_t *value_out)
|
||||||
|
{
|
||||||
|
struct v4l2_ext_control ctrl;
|
||||||
|
struct v4l2_ext_controls ctrls;
|
||||||
|
memset(&ctrl, 0, sizeof(ctrl));
|
||||||
|
memset(&ctrls, 0, sizeof(ctrls));
|
||||||
|
|
||||||
|
ctrl.id = id;
|
||||||
|
ctrls.controls = &ctrl;
|
||||||
|
ctrls.count = 1;
|
||||||
|
ctrls.which = V4L2_CTRL_WHICH_CUR_VAL;
|
||||||
|
|
||||||
|
if (ioctl(handle->fd, VIDIOC_G_EXT_CTRLS, &ctrls) < 0) {
|
||||||
|
return APP_SYSCALL_ERROR();
|
||||||
|
}
|
||||||
|
|
||||||
|
*value_out = ctrl.value;
|
||||||
|
return APP_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct App_Error v4l2_ctrl_set(
|
||||||
|
struct V4l2_Ctrl_Handle *handle,
|
||||||
|
uint32_t id,
|
||||||
|
int32_t value)
|
||||||
|
{
|
||||||
|
struct v4l2_ext_control ctrl;
|
||||||
|
struct v4l2_ext_controls ctrls;
|
||||||
|
memset(&ctrl, 0, sizeof(ctrl));
|
||||||
|
memset(&ctrls, 0, sizeof(ctrls));
|
||||||
|
|
||||||
|
ctrl.id = id;
|
||||||
|
ctrl.value = value;
|
||||||
|
ctrls.controls = &ctrl;
|
||||||
|
ctrls.count = 1;
|
||||||
|
ctrls.which = V4L2_CTRL_WHICH_CUR_VAL;
|
||||||
|
|
||||||
|
if (ioctl(handle->fd, VIDIOC_S_EXT_CTRLS, &ctrls) < 0) {
|
||||||
|
return APP_SYSCALL_ERROR();
|
||||||
|
}
|
||||||
|
|
||||||
|
return APP_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
const char *v4l2_ctrl_type_name(V4l2_Ctrl_Type type) {
|
||||||
|
switch (type) {
|
||||||
|
case CTRL_TYPE_INTEGER: return "int";
|
||||||
|
case CTRL_TYPE_BOOLEAN: return "bool";
|
||||||
|
case CTRL_TYPE_MENU: return "menu";
|
||||||
|
case CTRL_TYPE_BUTTON: return "button";
|
||||||
|
case CTRL_TYPE_INTEGER64: return "int64";
|
||||||
|
case CTRL_TYPE_CTRL_CLASS: return "class";
|
||||||
|
case CTRL_TYPE_STRING: return "string";
|
||||||
|
case CTRL_TYPE_BITMASK: return "bitmask";
|
||||||
|
case CTRL_TYPE_INTEGER_MENU: return "int-menu";
|
||||||
|
case CTRL_TYPE_UNKNOWN: /* fallthrough */
|
||||||
|
default: return "unknown";
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user