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

View 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

View 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";
}