diff --git a/.gitignore b/.gitignore index feead5b..819685a 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ reference/ +build/ diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..9e780ce --- /dev/null +++ b/Makefile @@ -0,0 +1,9 @@ +.PHONY: all clean cli + +all: cli + +cli: + $(MAKE) -C dev/cli + +clean: + rm -rf build/ diff --git a/conventions.md b/conventions.md index 222255c..0e837e4 100644 --- a/conventions.md +++ b/conventions.md @@ -2,7 +2,8 @@ ## 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) --- diff --git a/dev/cli/Makefile b/dev/cli/Makefile new file mode 100644 index 0000000..48ea0ca --- /dev/null +++ b/dev/cli/Makefile @@ -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 diff --git a/dev/cli/media_ctrl_cli.c b/dev/cli/media_ctrl_cli.c new file mode 100644 index 0000000..c247dd8 --- /dev/null +++ b/dev/cli/media_ctrl_cli.c @@ -0,0 +1,248 @@ +#include +#include +#include + +#include "error.h" +#include "media_ctrl.h" + +static void usage(const char *prog) { + fprintf(stderr, + "Usage: %s [args]\n" + "\n" + "Commands:\n" + " list List all /dev/media* devices\n" + " info Show device information\n" + " topology Show full pipeline topology\n" + " set-link : : <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: : : <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; +} diff --git a/dev/cli/v4l2_ctrl_cli.c b/dev/cli/v4l2_ctrl_cli.c new file mode 100644 index 0000000..b00eaa3 --- /dev/null +++ b/dev/cli/v4l2_ctrl_cli.c @@ -0,0 +1,206 @@ +#include +#include +#include + +#include "error.h" +#include "v4l2_ctrl.h" + +static void usage(const char *prog) { + fprintf(stderr, + "Usage: %s [args]\n" + "\n" + "Commands:\n" + " list List all /dev/video* devices\n" + " controls List all controls with current values\n" + " get Get a control value by ID (decimal or 0x hex)\n" + " set =\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 =, 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: \n"); + return 1; + } + return cmd_get(argv[2], argv[3]); + } + + if (strcmp(cmd, "set") == 0) { + if (argc < 4) { + fprintf(stderr, "set requires: =\n"); + return 1; + } + return cmd_set(argv[2], argv[3]); + } + + fprintf(stderr, "Unknown command: %s\n\n", cmd); + usage(argv[0]); + return 1; +} diff --git a/docs/cli/media_ctrl_cli.md b/docs/cli/media_ctrl_cli.md new file mode 100644 index 0000000..96a3c93 --- /dev/null +++ b/docs/cli/media_ctrl_cli.md @@ -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 ` + +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 ` + +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 : : <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. diff --git a/docs/cli/v4l2_ctrl_cli.md b/docs/cli/v4l2_ctrl_cli.md new file mode 100644 index 0000000..f7cbd7c --- /dev/null +++ b/docs/cli/v4l2_ctrl_cli.md @@ -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 ` + +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 ` + +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 =` + +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. diff --git a/include/error.h b/include/error.h new file mode 100644 index 0000000..097e79a --- /dev/null +++ b/include/error.h @@ -0,0 +1,59 @@ +#pragma once + +#include + +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__, \ + }) diff --git a/include/media_ctrl.h b/include/media_ctrl.h new file mode 100644 index 0000000..0d4a7cd --- /dev/null +++ b/include/media_ctrl.h @@ -0,0 +1,90 @@ +#pragma once + +#include +#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); diff --git a/include/v4l2_ctrl.h b/include/v4l2_ctrl.h new file mode 100644 index 0000000..d35a5f1 --- /dev/null +++ b/include/v4l2_ctrl.h @@ -0,0 +1,75 @@ +#pragma once + +#include +#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); diff --git a/src/modules/common/Makefile b/src/modules/common/Makefile new file mode 100644 index 0000000..dd5fc61 --- /dev/null +++ b/src/modules/common/Makefile @@ -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 diff --git a/src/modules/common/error.c b/src/modules/common/error.c new file mode 100644 index 0000000..3927684 --- /dev/null +++ b/src/modules/common/error.c @@ -0,0 +1,29 @@ +#include +#include + +#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; + } +} diff --git a/src/modules/media_ctrl/Makefile b/src/modules/media_ctrl/Makefile new file mode 100644 index 0000000..c51ac12 --- /dev/null +++ b/src/modules/media_ctrl/Makefile @@ -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 diff --git a/src/modules/media_ctrl/media_ctrl.c b/src/modules/media_ctrl/media_ctrl.c new file mode 100644 index 0000000..3eeb840 --- /dev/null +++ b/src/modules/media_ctrl/media_ctrl.c @@ -0,0 +1,283 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/* + * 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"; +} diff --git a/src/modules/v4l2_ctrl/Makefile b/src/modules/v4l2_ctrl/Makefile new file mode 100644 index 0000000..d321706 --- /dev/null +++ b/src/modules/v4l2_ctrl/Makefile @@ -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 diff --git a/src/modules/v4l2_ctrl/v4l2_ctrl.c b/src/modules/v4l2_ctrl/v4l2_ctrl.c new file mode 100644 index 0000000..8b36f6c --- /dev/null +++ b/src/modules/v4l2_ctrl/v4l2_ctrl.c @@ -0,0 +1,292 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/* 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"; + } +}