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:
2026-03-25 21:40:37 +00:00
parent bf18054a2c
commit a29c556851
17 changed files with 1650 additions and 1 deletions

29
dev/cli/Makefile Normal file
View 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
View 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
View 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;
}