/* SPDX-License-Identifier: LGPL-2.1-or-later */
|
/*
|
* ConfigFS Gadget device handling
|
*
|
* Copyright (C) 2020 Rockchip Electronics Co., Ltd.
|
*
|
*/
|
|
/* To provide basename and asprintf from the GNU library. */
|
#define _GNU_SOURCE
|
|
#include <unistd.h>
|
#include <dirent.h>
|
#include <stdio.h>
|
#include <stdbool.h>
|
#include <string.h>
|
#include <stdlib.h>
|
#include <fcntl.h>
|
#include <pthread.h>
|
#include <errno.h>
|
#include <glob.h>
|
#include <linux/videodev2.h>
|
|
#include "uvc_configfs.h"
|
|
#define V4L2_PIX_FMT_H265 v4l2_fourcc('H', '2', '6', '5') /* H265 with start codes */
|
|
#define UVC_FUNCTION_NAME "uvc*"
|
|
#define UVC_GUID_FORMAT_MJPEG \
|
{ 'M', 'J', 'P', 'G', 0x00, 0x00, 0x10, 0x00, \
|
0x80, 0x00, 0x00, 0xaa, 0x00, 0x38, 0x9b, 0x71}
|
#define UVC_GUID_FORMAT_YUY2 \
|
{ 'Y', 'U', 'Y', '2', 0x00, 0x00, 0x10, 0x00, \
|
0x80, 0x00, 0x00, 0xaa, 0x00, 0x38, 0x9b, 0x71}
|
#define UVC_GUID_FORMAT_NV12 \
|
{ 'N', 'V', '1', '2', 0x00, 0x00, 0x10, 0x00, \
|
0x80, 0x00, 0x00, 0xaa, 0x00, 0x38, 0x9b, 0x71}
|
#define UVC_GUID_FORMAT_H264 \
|
{ 'H', '2', '6', '4', 0x00, 0x00, 0x10, 0x00, \
|
0x80, 0x00, 0x00, 0xaa, 0x00, 0x38, 0x9b, 0x71}
|
#define UVC_GUID_FORMAT_H265 \
|
{ 'H', '2', '6', '5', 0x00, 0x00, 0x10, 0x00, \
|
0x80, 0x00, 0x00, 0xaa, 0x00, 0x38, 0x9b, 0x71}
|
|
#define ARRAY_SIZE(arr) (sizeof(arr) / sizeof((arr)[0]))
|
|
struct uvc_function_format_info {
|
uint8_t guid[16];
|
uint32_t fcc;
|
};
|
|
static struct uvc_function_format_info uvc_formats[] = {
|
{
|
.guid = UVC_GUID_FORMAT_YUY2,
|
.fcc = V4L2_PIX_FMT_YUYV,
|
},
|
{
|
.guid = UVC_GUID_FORMAT_MJPEG,
|
.fcc = V4L2_PIX_FMT_MJPEG,
|
},
|
{
|
.guid = UVC_GUID_FORMAT_H264,
|
.fcc = V4L2_PIX_FMT_H264,
|
},
|
{
|
.guid = UVC_GUID_FORMAT_H265,
|
.fcc = V4L2_PIX_FMT_H265,
|
},
|
{
|
.guid = UVC_GUID_FORMAT_NV12,
|
.fcc = V4L2_PIX_FMT_NV12,
|
}
|
};
|
|
/* -----------------------------------------------------------------------------
|
* Path handling and support
|
*/
|
static char *path_join(const char *dirname, const char *name)
|
{
|
char *path;
|
int ret;
|
|
ret = asprintf(&path, "%s/%s", dirname, name);
|
|
/*
|
* asprintf returns -1 on allocation or other errors, leaving 'path'
|
* undefined. We shouldn't even call free(path) here. We want to return
|
* NULL on error, so we must manually set it.
|
*/
|
if (ret < 0)
|
path = NULL;
|
|
return path;
|
}
|
|
static char *path_glob_index_match(const char *g, unsigned int index)
|
{
|
glob_t globbuf;
|
char *match = NULL;
|
|
glob(g, 0, NULL, &globbuf);
|
|
if (globbuf.gl_pathc > index)
|
match = strdup(globbuf.gl_pathv[index]);
|
|
globfree(&globbuf);
|
|
return match;
|
}
|
|
/*
|
* Find and return the full path of the first directory entry that satisfies
|
* the match function.
|
*/
|
static char *dir_first_match(const char *dir, int(*match)(const struct dirent *))
|
{
|
struct dirent **entries;
|
unsigned int i;
|
int n_entries;
|
char *path;
|
|
n_entries = scandir(dir, &entries, match, alphasort);
|
if (n_entries < 0)
|
return NULL;
|
|
if (n_entries == 0) {
|
free(entries);
|
return NULL;
|
}
|
|
path = path_join(dir, entries[0]->d_name);
|
|
for (i = 0; i < (unsigned int)n_entries; ++i)
|
free(entries[i]);
|
|
free(entries);
|
|
return path;
|
}
|
|
/* -----------------------------------------------------------------------------
|
* Attribute handling
|
*/
|
static int attribute_read(const char *path, const char *file, void *buf,
|
unsigned int len)
|
{
|
char *f;
|
int ret;
|
int fd;
|
|
f = path_join(path, file);
|
if (!f)
|
return -ENOMEM;
|
|
fd = open(f, O_RDONLY);
|
free(f);
|
if (fd == -1) {
|
printf("Failed to open attribute %s: %s\n", file,
|
strerror(errno));
|
return -ENOENT;
|
}
|
|
ret = read(fd, buf, len);
|
close(fd);
|
|
if (ret < 0) {
|
printf("Failed to read attribute %s: %s\n", file,
|
strerror(errno));
|
return -ENODATA;
|
}
|
|
return len;
|
}
|
|
static int attribute_read_uint(const char *path, const char *file,
|
unsigned int *val)
|
{
|
char buf[11] = {0};
|
char *endptr;
|
int ret;
|
|
ret = attribute_read(path, file, buf, sizeof(buf) - 1);
|
if (ret < 0)
|
return ret;
|
|
buf[ret] = '\0';
|
errno = 0;
|
|
/* base 0: Autodetect hex, octal, decimal. */
|
*val = strtoul(buf, &endptr, 0);
|
if (errno)
|
return -errno;
|
|
if (endptr == buf)
|
return -ENODATA;
|
|
return 0;
|
}
|
|
static char *attribute_read_str(const char *path, const char *file)
|
{
|
char buf[1024] = {0};
|
char *p;
|
int ret;
|
|
ret = attribute_read(path, file, buf, sizeof(buf) - 1);
|
if (ret < 0)
|
return NULL;
|
|
buf[ret] = '\0';
|
|
p = strrchr(buf, '\n');
|
if(p == NULL)
|
return NULL;
|
if (p != buf)
|
*p = '\0';
|
|
return strdup(buf);
|
}
|
|
static char *configfs_find_uvc_function(unsigned int index)
|
{
|
const char *target = UVC_FUNCTION_NAME;
|
const char *format;
|
char *func_path;
|
char *path;
|
int ret;
|
|
format = "%s/usb_gadget/*/functions/%s";
|
ret = asprintf(&path, format, "/sys/kernel/config", target);
|
if (!ret)
|
return NULL;
|
|
func_path = path_glob_index_match(path, index);
|
|
free(path);
|
|
return func_path;
|
}
|
|
static int udc_find_video_device(const char *udc, const char *function)
|
{
|
char *vpath;
|
char *video = NULL;
|
glob_t globbuf;
|
int video_id = -1;
|
unsigned int i;
|
int ret;
|
|
ret = asprintf(&vpath,
|
"/sys/class/udc/%s/device/gadget/video4linux/video*",
|
udc ? udc : "*");
|
if (!ret)
|
return -1;
|
|
glob(vpath, 0, NULL, &globbuf);
|
free(vpath);
|
|
for (i = 0; i < globbuf.gl_pathc; ++i) {
|
char *config;
|
bool match;
|
|
/* Match on the first if no search string. */
|
if (!function)
|
break;
|
|
config = attribute_read_str(globbuf.gl_pathv[i],
|
"function_name");
|
match = strcmp(function, config) == 0;
|
free(config);
|
if (match)
|
break;
|
}
|
|
if (i < globbuf.gl_pathc) {
|
const char *v = basename(globbuf.gl_pathv[i]);
|
video = path_join("/dev", v);
|
if (sscanf(video, "/dev/video%d", &video_id) != 1)
|
video_id = -1;
|
free(video);
|
}
|
|
globfree(&globbuf);
|
|
return video_id;
|
}
|
|
/*
|
* configfs_free_uvc_function - Free a uvc_function_config object
|
* @fc: The uvc_function_config to be freed
|
*
|
* Free the given @fc function previously allocated by a call to
|
* configfs_parse_uvc_function().
|
*/
|
void configfs_free_uvc_function(struct uvc_function_config *fc)
|
{
|
unsigned int i, j;
|
|
free(fc->udc);
|
free(fc->dev_name);
|
|
for (i = 0; i < fc->streaming.num_formats; ++i) {
|
struct uvc_function_config_format *format =
|
&fc->streaming.formats[i];
|
|
for (j = 0; j < format->num_frames; ++j) {
|
struct uvc_function_config_frame *frame =
|
&format->frames[j];
|
|
free(frame->intervals);
|
}
|
free(format->frames);
|
}
|
free(fc->streaming.formats);
|
free(fc);
|
}
|
|
#define configfs_parse_child(parent, child, cfg, parse) \
|
({ \
|
char *__path; \
|
int __ret; \
|
\
|
__path = path_join((parent), (child)); \
|
if (__path) { \
|
__ret = parse(__path, (cfg)); \
|
free(__path); \
|
} else { \
|
__ret = -ENOMEM; \
|
} \
|
\
|
__ret; \
|
})
|
|
static int configfs_parse_interface(const char *path,
|
struct uvc_function_config_interface *cfg)
|
{
|
int ret;
|
|
ret = attribute_read_uint(path, "bInterfaceNumber",
|
&cfg->bInterfaceNumber);
|
|
return ret;
|
}
|
|
static int configfs_parse_control(const char *path,
|
struct uvc_function_config_control *cfg)
|
{
|
int ret;
|
|
ret = configfs_parse_interface(path, &cfg->intf);
|
|
return ret;
|
}
|
|
static int configfs_parse_streaming_frame(const char *path,
|
struct uvc_function_config_frame *frame)
|
{
|
char *intervals;
|
char *p;
|
int ret = 0;
|
|
ret = ret ? : attribute_read_uint(path, "bFrameIndex", &frame->index);
|
ret = ret ? : attribute_read_uint(path, "wWidth", &frame->width);
|
ret = ret ? : attribute_read_uint(path, "wHeight", &frame->height);
|
if (ret)
|
return ret;
|
|
intervals = attribute_read_str(path, "dwFrameInterval");
|
if (!intervals)
|
return -EINVAL;
|
|
for (p = intervals; *p; ) {
|
unsigned int interval;
|
unsigned int *mem;
|
char *endp;
|
size_t size;
|
|
interval = strtoul(p, &endp, 10);
|
if (*endp != '\0' && *endp != '\n') {
|
ret = -EINVAL;
|
break;
|
}
|
|
p = *endp ? endp + 1 : endp;
|
|
size = sizeof *frame->intervals * (frame->num_intervals + 1);
|
mem = realloc(frame->intervals, size);
|
if (!mem) {
|
ret = -ENOMEM;
|
break;
|
}
|
frame->intervals = mem;
|
frame->intervals[frame->num_intervals++] = interval;
|
}
|
free(intervals);
|
|
return ret;
|
}
|
|
static int frame_filter(const struct dirent *ent)
|
{
|
/* Accept all directories but "." and "..". */
|
if (ent->d_type != DT_DIR)
|
return 0;
|
if (!strcmp(ent->d_name, "."))
|
return 0;
|
if (!strcmp(ent->d_name, ".."))
|
return 0;
|
return 1;
|
}
|
|
static int frame_compare(const void *a, const void *b)
|
{
|
const struct uvc_function_config_frame *fa = a;
|
const struct uvc_function_config_frame *fb = b;
|
|
if (fa->index < fb->index)
|
return -1;
|
else if (fa->index == fb->index)
|
return 0;
|
else
|
return 1;
|
}
|
|
static int configfs_parse_streaming_format(const char *path,
|
struct uvc_function_config_format *format)
|
{
|
struct dirent **entries;
|
char link_target[1024] = {0};
|
char *segment;
|
unsigned int i;
|
int n_entries;
|
int ret;
|
|
ret = attribute_read_uint(path, "bFormatIndex", &format->index);
|
if (ret < 0)
|
return ret;
|
|
ret = readlink(path, link_target, sizeof(link_target) - 1);
|
if (ret < 0)
|
return ret;
|
|
link_target[ret] = '\0';
|
|
/*
|
* Extract the second-to-last path component of the link target,
|
* which contains the format descriptor type name as exposed by
|
* the UVC function driver.
|
*/
|
segment = strrchr(link_target, '/');
|
if (!segment)
|
return -EINVAL;
|
*segment = '\0';
|
segment = strrchr(link_target, '/');
|
if (!segment)
|
return -EINVAL;
|
segment++;
|
if (!strcmp(segment, "framebased")) {
|
ret = attribute_read(path, "guidFormat", format->guid,
|
sizeof(format->guid));
|
} else if (!strcmp(segment, "mjpeg")) {
|
static const uint8_t guid[16] = UVC_GUID_FORMAT_MJPEG;
|
memcpy(format->guid, guid, 16);
|
} else if (!strcmp(segment, "uncompressed")) {
|
ret = attribute_read(path, "guidFormat", format->guid,
|
sizeof(format->guid));
|
if (ret < 0)
|
return ret;
|
} else {
|
return -EINVAL;
|
}
|
|
for (i = 0; i < ARRAY_SIZE(uvc_formats); ++i) {
|
if (!memcmp(uvc_formats[i].guid, format->guid, 16)) {
|
format->fcc = uvc_formats[i].fcc;
|
break;
|
}
|
}
|
|
/* Find all entries corresponding to a frame and parse them. */
|
n_entries = scandir(path, &entries, frame_filter, alphasort);
|
if (n_entries < 0)
|
return -errno;
|
|
if (n_entries == 0) {
|
free(entries);
|
return -EINVAL;
|
}
|
|
format->num_frames = n_entries;
|
format->frames = calloc(sizeof *format->frames, format->num_frames);
|
if (!format->frames)
|
return -ENOMEM;
|
|
for (i = 0; i < (unsigned int)n_entries; ++i) {
|
char *frame;
|
|
frame = path_join(path, entries[i]->d_name);
|
if (!frame) {
|
ret = -ENOMEM;
|
goto done;
|
}
|
|
ret = configfs_parse_streaming_frame(frame, &format->frames[i]);
|
free(frame);
|
if (ret < 0)
|
goto done;
|
}
|
|
/* Sort the frames by index. */
|
qsort(format->frames, format->num_frames, sizeof *format->frames,
|
frame_compare);
|
|
done:
|
for (i = 0; i < (unsigned int)n_entries; ++i)
|
free(entries[i]);
|
free(entries);
|
|
return ret;
|
}
|
|
static int format_filter(const struct dirent *ent)
|
{
|
char *path;
|
bool valid;
|
|
/*
|
* Accept all links that point to a directory containing a
|
* "bFormatIndex" file.
|
*/
|
if (ent->d_type != DT_LNK)
|
return 0;
|
|
path = path_join(ent->d_name, "bFormatIndex");
|
if (!path)
|
return 0;
|
|
valid = access(path, R_OK);
|
free(path);
|
return valid;
|
}
|
|
static int format_compare(const void *a, const void *b)
|
{
|
const struct uvc_function_config_format *fa = a;
|
const struct uvc_function_config_format *fb = b;
|
|
if (fa->index < fb->index)
|
return -1;
|
else if (fa->index == fb->index)
|
return 0;
|
else
|
return 1;
|
}
|
|
static int configfs_parse_streaming_header(const char *path,
|
struct uvc_function_config_streaming *cfg)
|
{
|
struct dirent **entries;
|
unsigned int i;
|
int n_entries;
|
int ret;
|
|
/* Find all entries corresponding to a format and parse them. */
|
n_entries = scandir(path, &entries, format_filter, alphasort);
|
if (n_entries < 0)
|
return -errno;
|
|
if (n_entries == 0) {
|
free(entries);
|
return -EINVAL;
|
}
|
|
cfg->num_formats = n_entries;
|
cfg->formats = calloc(sizeof *cfg->formats, cfg->num_formats);
|
if (!cfg->formats)
|
return -ENOMEM;
|
|
for (i = 0; i < (unsigned int)n_entries; ++i) {
|
char *format;
|
|
format = path_join(path, entries[i]->d_name);
|
if (!format) {
|
ret = -ENOMEM;
|
goto done;
|
}
|
|
ret = configfs_parse_streaming_format(format, &cfg->formats[i]);
|
free(format);
|
if (ret < 0)
|
goto done;
|
}
|
|
/* Sort the formats by index. */
|
qsort(cfg->formats, cfg->num_formats, sizeof *cfg->formats,
|
format_compare);
|
|
done:
|
for (i = 0; i < (unsigned int)n_entries; ++i)
|
free(entries[i]);
|
free(entries);
|
|
return ret;
|
}
|
|
static int link_filter(const struct dirent *ent)
|
{
|
/* Accept all links. */
|
return ent->d_type == DT_LNK;
|
}
|
|
static int configfs_parse_streaming(const char *path,
|
struct uvc_function_config_streaming *cfg)
|
{
|
char *header;
|
char *class;
|
int ret;
|
|
ret = configfs_parse_interface(path, &cfg->intf);
|
if (ret < 0)
|
return ret;
|
/*
|
* Handle the high-speed class descriptors only for now. Find the first
|
* link to the class descriptors.
|
*/
|
class = path_join(path, "class/hs");
|
if (!class)
|
return -ENOMEM;
|
|
header = dir_first_match(class, link_filter);
|
free(class);
|
if (!header)
|
return -EINVAL;
|
|
ret = configfs_parse_streaming_header(header, cfg);
|
free(header);
|
return ret;
|
}
|
|
static int configfs_parse_uvc(const char *fpath,
|
struct uvc_function_config *fc)
|
{
|
int ret = 0;
|
|
ret = ret ? : configfs_parse_child(fpath, "control", &fc->control,
|
configfs_parse_control);
|
ret = ret ? : configfs_parse_child(fpath, "streaming", &fc->streaming,
|
configfs_parse_streaming);
|
|
/*
|
* These parameters should be part of the streaming interface in
|
* ConfigFS, but for legacy reasons they are located directly in the
|
* function directory.
|
*/
|
ret = ret ? : attribute_read_uint(fpath, "streaming_interval",
|
&fc->streaming.ep.bInterval);
|
ret = ret ? : attribute_read_uint(fpath, "streaming_maxburst",
|
&fc->streaming.ep.bMaxBurst);
|
ret = ret ? : attribute_read_uint(fpath, "streaming_maxpacket",
|
&fc->streaming.ep.wMaxPacketSize);
|
|
return ret;
|
}
|
|
struct uvc_function_config *configfs_parse_uvc_function(unsigned int index)
|
{
|
struct uvc_function_config *fc;
|
char *fpath;
|
char *function;
|
int ret = 0;
|
|
fc = malloc(sizeof *fc);
|
if (fc == NULL)
|
return NULL;
|
|
memset(fc, 0, sizeof *fc);
|
|
/* Find the function in ConfigFS. */
|
fpath = configfs_find_uvc_function(index);
|
if (!fpath) {
|
free(fc);
|
return NULL;
|
}
|
|
function = basename(fpath);
|
|
fc->dev_name = attribute_read_str(fpath, "device_name");
|
if(fc->dev_name == NULL)
|
fc->dev_name = "UVC CAMERA";
|
fc->udc = attribute_read_str(fpath, "../../UDC");
|
fc->video = udc_find_video_device(fc->udc, function);
|
printf("fc->dev_name:%s, fc->udc:%s, fc->video:%d \n",fc->dev_name,fc->udc,fc->video);
|
if (fc->video < 0) {
|
ret = -ENODEV;
|
goto done;
|
}
|
fc->index = index;
|
|
ret = configfs_parse_uvc(fpath, fc);
|
|
done:
|
if (ret) {
|
configfs_free_uvc_function(fc);
|
fc = NULL;
|
}
|
|
free(fpath);
|
|
return fc;
|
}
|