[libcamera-devel] [PATCH 2/2] lib: Add V4L2 Device object

Kieran Bingham kieran.bingham at ideasonboard.com
Sat Dec 22 21:30:37 CET 2018


Hi Jacopo,

Thanks for the review,

On 21/12/2018 16:25, jacopo mondi wrote:
> Hi Kieran,
>     thanks for the patch
> 
> On Fri, Dec 21, 2018 at 12:37:24PM +0000, Kieran Bingham wrote:
>> Provide a helper V4L2 device object capable of interacting with the
>> V4L2 Linux Kernel APIs.
>>
>> A test suite is added at test/v4l2_device/ to utilise and validate the
>> code base.
>>
>> Signed-off-by: Kieran Bingham <kieran.bingham at ideasonboard.com>
>> ---
>>  src/libcamera/include/v4l2_device.h   |  36 +++++++
>>  src/libcamera/meson.build             |   2 +
>>  src/libcamera/v4l2_device.cpp         | 137 ++++++++++++++++++++++++++
> 
> I would really split test and library code in 2 differen commits.

I'm happy to go with group consensus here, but I felt that tests and
code could be in the same commit here, as the test directly affects the
code added.

This means that when doing rebase actions or such, I can do 'git rebase
-i -x "ninja; ninja test"' and validate every commit as I rework things.

(I have a script which does that for me)


>>  test/meson.build                      |   2 +
>>  test/v4l2_device/double_open.cpp      |  32 ++++++
>>  test/v4l2_device/meson.build          |  12 +++
>>  test/v4l2_device/v4l2_device_test.cpp |  36 +++++++
>>  test/v4l2_device/v4l2_device_test.h   |  31 ++++++
>>  8 files changed, 288 insertions(+)
>>  create mode 100644 src/libcamera/include/v4l2_device.h
>>  create mode 100644 src/libcamera/v4l2_device.cpp
>>  create mode 100644 test/v4l2_device/double_open.cpp
>>  create mode 100644 test/v4l2_device/meson.build
>>  create mode 100644 test/v4l2_device/v4l2_device_test.cpp
>>  create mode 100644 test/v4l2_device/v4l2_device_test.h
>>
>> diff --git a/src/libcamera/include/v4l2_device.h b/src/libcamera/include/v4l2_device.h
>> new file mode 100644
>> index 000000000000..619d932d3c82
>> --- /dev/null
>> +++ b/src/libcamera/include/v4l2_device.h
>> @@ -0,0 +1,36 @@
>> +/* SPDX-License-Identifier: LGPL-2.1-or-later */
>> +/*
>> + * Copyright (C) 2018, Google Inc.
>> + *
>> + * v4l2_device.h - V4L2 Device API Abstractions
>> + */
>> +#ifndef __LIBCAMERA_V4L2_DEVICE_H__
>> +#define __LIBCAMERA_V4L2_DEVICE_H__
>> +
>> +#include <string>
>> +
>> +#include <linux/videodev2.h>
>> +
>> +namespace libcamera {
>> +
>> +class V4L2Device
>> +{
>> +public:
>> +	V4L2Device(const std::string &);
>> +	~V4L2Device();
>> +
>> +	bool isOpen();
>> +	int open();
>> +	void close();
>> +
>> +	int queryCap();
>> +
>> +private:
>> +	std::string device_;
>> +	int fd_;
>> +	int capabilities_;
>> +};
>> +
>> +}
> 
> nit:
> } /* namespace libcamera */
> 

Ah yes - that might be nice.

How did I miss this?
Do we have this in our templates?

Hrm ... nope, this end comment isn't there.

I've added it to the template document.


>> +
>> +#endif /* __LIBCAMERA_V4L2_DEVICE_H__ */
>> diff --git a/src/libcamera/meson.build b/src/libcamera/meson.build
>> index f632eb5dd779..bbaf9a05ec2c 100644
>> --- a/src/libcamera/meson.build
>> +++ b/src/libcamera/meson.build
>> @@ -1,11 +1,13 @@
>>  libcamera_sources = files([
>>      'log.cpp',
>>      'main.cpp',
>> +    'v4l2_device.cpp',
>>  ])
>>
>>  libcamera_headers = files([
>>      'include/log.h',
>>      'include/utils.h',
>> +    'include/v4l2_device.h',
>>  ])
>>
>>  libcamera_internal_includes =  include_directories('include')
>> diff --git a/src/libcamera/v4l2_device.cpp b/src/libcamera/v4l2_device.cpp
>> new file mode 100644
>> index 000000000000..eea2514f343c
>> --- /dev/null
>> +++ b/src/libcamera/v4l2_device.cpp
>> @@ -0,0 +1,137 @@
>> +/* SPDX-License-Identifier: LGPL-2.1-or-later */
>> +/*
>> + * Copyright (C) 2018, Google Inc.
>> + *
>> + * v4l2_device.cpp - V4L2 Device API
>> + */
>> +
>> +#include <fcntl.h>
>> +#include <string.h>
>> +#include <unistd.h>
>> +
>> +#include <sys/ioctl.h>
>> +#include <sys/mman.h>
>> +
>> +#include "log.h"
>> +#include "v4l2_device.h"
>> +
>> +/**
>> + * \file v4l2_device.cpp
> 
> file v4l2_device.h to document here what is defined in the header file

Oh - So this is supposed to reference the header not declare the current
file. I guess that kind of makes sense :) Always pointless to state the
name of the current file.

> 
>> + * \brief V4L2 Device API
>> + */
>> +namespace libcamera {
>> +
>> +/**
>> + * \class V4L2Device
>> + * \brief V4L2Device object and API.
>> + *
>> + * The V4L2 Device API class models a single instance of a v4l2 device node.
>> + */
>> +
>> +/**
>> + * \fn V4L2Device::V4L2Device(device)
> 
> nit: parameter type?

I believe doxygen collects that automatically?

It generates this in the output:
  libcamera::V4L2Device::V4L2Device(const std::string & device)


>> + *
>> + * Default constructor for a V4L2Device object. The \a device specifies a
>> + * string to representation of the device object.
> 
> My poor English skill do not allow me to say if this is a typo or it
> actually makes sense :)

Ahem - there's certainly a typo there - good spot.

It's also not necessarily correct, and is too close from my Camera class
constructor text.

I'll adapt this to :

 * Default constructor for a V4L2Device object. The \a device specifies
 * the path to the video device node.

> 
>> + */
>> +V4L2Device::V4L2Device(const std::string &device)
>> +{
>> +	device_ = device;
>> +	fd_ = -1;
>> +	capabilities_ = 0;
>> +
>> +	LOG(Debug) << "V4L2Device Constructed for " << device_;
> 
> Not sure a debug printout is worth here

Certainly not for long... they're just here for early dev.

I can just drop these - they're not useful.

> 
>> +}
>> +
>> +V4L2Device::~V4L2Device()
>> +{
>> +	close();
>> +
>> +	LOG(Debug) << "V4L2Device Destroyed";
> 
> ditto
> 
>> +}
>> +
>> +/**
>> + * \fn V4L2Device::isOpen(())
> 
> one () in excess?

Not sure how that happened?

> 
>> + *
>> + * Checks to see if we have successfully opened a v4l2 video device.
>> + */
>> +bool V4L2Device::isOpen()
>> +{
>> +	return (fd_ != -1);
> 
> nit: no need to () braces

Can I keep them ?

Returning more than a simple variable or statement 'openly' feels
horrible to me ...


>> +}
>> +
>> +/**
>> + * \fn V4L2Device::open()
>> + *
>> + * Opens a v4l2 device and queries properties from the device.
>> + */
>> +int V4L2Device::open()
>> +{
>> +	int ret;
>> +
>> +	if (isOpen()) {
>> +		LOG(Error) << "Device already open";
>> +		return -EBADF;
>> +	}
>> +
>> +	fd_ = ::open(device_.c_str(), O_RDWR);
>> +	if (fd_ < 0) {
>> +		LOG(Error) << "Failed to open V4L2 device " << device_
>> +			   << " : " << strerror(errno);
>> +		return -errno;
>> +	}
>> +
>> +	ret = queryCap();
> 
> I don't like it much either, but this might have been "int ret = "

Hrm ... that certainly is late instantiation.

Is that what we're going for ?


> 
>> +	if (ret)
>> +		return ret;
>> +
>> +	if (!(capabilities_ & V4L2_CAP_STREAMING)) {
> 
> I wonder if it wouldn't be better to return the capabilities from
> queryCap, and assign them to the class member capabilities_ after this
> check here.
> 
> Looking at the code I -knew- that the queryCap() function had modified
> the capabilities_ and then you're inspecting them here, but it's all a
> bit implicit to me...

How will we determine if the ioctl call succeeded?
 (while avoiding exceptions?)

Capabilities flag can return any free-form int value. (ok - so it's
restricted to the flags available in V4L2 ... but... that's very
function specific)



>> +		LOG(Error) << "Device does not support streaming IO";
>> +		return -EINVAL;
>> +	}
>> +
>> +	return 0;
>> +}
>> +
>> +/**
>> + * \fn V4L2Device::close()
>> + *
>> + * Close a v4l2 device handle.
>> + */
>> +void V4L2Device::close()
>> +{
>> +	if (fd_ < 0)
>> +		return;
>> +
>> +	::close(fd_);
>> +	fd_ = -1;
>> +}
>> +
>> +/**
>> + * \fn V4L2Device::queryCap()
>> + *
>> + * Obtains the capabilities information from the V4L2 Device.
>> + */
>> +int V4L2Device::queryCap()
>> +{
>> +	struct v4l2_capability cap = {};
>> +	int ret;
>> +
>> +	if (!isOpen()) {
>> +		LOG(Error) << "Attempt to query unopened device";
>> +		return -EBADF;
>> +	}
>> +
>> +	ret = ioctl(fd_, VIDIOC_QUERYCAP, &cap);
>> +	if (ret < 0) {
>> +		LOG(Error) << "Failed to query device capabilities";
>> +		return -1;
> 
> It would be useful to return the errno number and possibly print it
> out with strerror

Good point!

Should this be -errno? Or just return ret?


The more I think about it - the more I'd like to wrap the ioctl call in
the class (or more likely a Device base class) which will handle the
error checks...

That could then be a parent class for your MediaDevice object too, and
the expected V4L2SubDevice ...

What are your thoughts? Over-abstracting? or useful ...


> 
>> +	}
>> +
>> +	capabilities_ = cap.capabilities & V4L2_CAP_DEVICE_CAPS
>> +		      ? cap.device_caps : cap.capabilities;
>> +
>> +	return 0;
>> +}
>> +
>> +} /* namespace libcamera */
>> diff --git a/test/meson.build b/test/meson.build
>> index 754527324c7d..2bc76c289eea 100644
>> --- a/test/meson.build
>> +++ b/test/meson.build
>> @@ -12,6 +12,8 @@ test_includes_internal = [
>>    libcamera_internal_includes,
>>  ]
>>
>> +subdir('v4l2_device')
>> +
>>  public_tests = [
>>    [ 'test_init',      'init.cpp' ],
> 
> Not related to this, but weird spacing here

This was intentional ... pre-allocation of table spacing. (The table
will have multiple entries...)

> 
>>  ]
>> diff --git a/test/v4l2_device/double_open.cpp b/test/v4l2_device/double_open.cpp
>> new file mode 100644
>> index 000000000000..1b7c7bfe14b8
>> --- /dev/null
>> +++ b/test/v4l2_device/double_open.cpp
>> @@ -0,0 +1,32 @@
>> +/* SPDX-License-Identifier: GPL-2.0-or-later */
>> +/*
>> + * Copyright (C) 2018, Google Inc.
>> + *
>> + * libcamera V4L2 API tests
>> + */
>> +
>> +#include "v4l2_device_test.h"
>> +
>> +class DoubleOpen : public V4L2DeviceTest
>> +{
>> +protected:
>> +	int run()
>> +	{
>> +		int ret;
>> +
>> +		/*
>> +		 * Expect failure: The device has already been opened by the
>> +		 * V4L2DeviceTest base class
>> +		 */
>> +		ret = dev->open();
>> +		if (!ret) {
>> +			fprintf(stderr, "Double open erroneously succeeded\n");
>> +			dev->close();
>> +			return TEST_FAIL;
>> +		}
>> +
>> +		return TEST_PASS;
>> +	}
> 
> nit: < one indent level

Oh -- for all class functions?


>> +};
> 
> I welcome testing, but do we really need tests to make sure a function
> fails when called twice? 

Yes - I felt this was important.

It's there because once I added the open() and close() calls - it
provided a way to leak fd's if you call open(), open().

The first fd would get silently dropped ...

So I added a test to call open twice, then I added the isOpen() checks.

In other words - the fix for the leak came about directly because I
added the double_open test...


Another reason we I think we should base class the common device
operations (then this test would only apply to the Device object unit
tests anyway.)


> I'm sure you could have sketched it by
> calling open twice in the V4L2DeviceTest class

No - the V4L2DeviceTest class is the base class common to all further
tests for the V4L2Device Object.

I have other patches which add further tests using the V4L2DeviceTest
base class.

>> +
>> +TEST_REGISTER(DoubleOpen);
>> diff --git a/test/v4l2_device/meson.build b/test/v4l2_device/meson.build
>> new file mode 100644
>> index 000000000000..41675a303498
>> --- /dev/null
>> +++ b/test/v4l2_device/meson.build
>> @@ -0,0 +1,12 @@
>> +# Tests are listed in order of complexity.
>> +# They are not alphabetically sorted.
>> +v4l2_device_tests = [
>> +  [ 'double_open',        'double_open.cpp' ],
> 
> Again weird spacing, I'm now thinking this is intentional and I don't
> know something

Table pre-allocation for multiple tests.

This isn't my only test, and I'd rather not have to reformat the table
if I add tests with longer filenames.

> 
>> +]
>> +
>> +foreach t : v4l2_device_tests
>> +  exe = executable(t[0], [t[1], 'v4l2_device_test.cpp'],
>> +		   link_with : test_libraries,
>> +		   include_directories : test_includes_internal)
>> +  test(t[0], exe, suite: 'v4l2_device', is_parallel: false)
>> +endforeach
>> diff --git a/test/v4l2_device/v4l2_device_test.cpp b/test/v4l2_device/v4l2_device_test.cpp
>> new file mode 100644
>> index 000000000000..ae317a9519c5
>> --- /dev/null
>> +++ b/test/v4l2_device/v4l2_device_test.cpp
>> @@ -0,0 +1,36 @@
>> +/* SPDX-License-Identifier: GPL-2.0-or-later */
>> +/*
>> + * Copyright (C) 2018, Google Inc.
>> + *
>> + * libcamera V4L2 API tests
>> + */
>> +
>> +#include "v4l2_device_test.h"
>> +
>> +using namespace libcamera;
>> +
>> +V4L2DeviceTest::V4L2DeviceTest()
>> +{
>> +	dev = nullptr;
>> +}
>> +
>> +int V4L2DeviceTest::init()
>> +{
>> +	const char *device = "/dev/video0";
>> +
>> +	/* Validate the device node exists. */
>> +	if (!path_exists(device))
>> +		return TEST_SKIP;
>> +
>> +	dev = new V4L2Device(device);
>> +	if (!dev)
> 
> You should call cleanup here if you fail
> Ah no, the Test class should call it if init() fails!
> Ah no^2, your constructor cannot fail (good!). My previous comment
> still hold though.
> 

Should I instead call cleanup() from my destructor ?



>> +		return TEST_FAIL;
>> +
>> +	return dev->open();
>> +}
>> +
>> +void V4L2DeviceTest::cleanup()
>> +{
>> +	if (dev)
>> +		delete dev;
>> +};
>> diff --git a/test/v4l2_device/v4l2_device_test.h b/test/v4l2_device/v4l2_device_test.h
>> new file mode 100644
>> index 000000000000..4374ddc7e932
>> --- /dev/null
>> +++ b/test/v4l2_device/v4l2_device_test.h
>> @@ -0,0 +1,31 @@
>> +/* SPDX-License-Identifier: GPL-2.0-or-later */
>> +/*
>> + * Copyright (C) 2018, Google Inc.
>> + *
>> + * vl42device_test.h - libcamera v4l2device test base class
>> + */
>> +
>> +#include "test.h"
>> +#include "v4l2_device.h"
> 
> Includes after the inclusion guards

Ack.


>> +
>> +#ifndef __LIBCAMERA_V4L2_DEVICE_TEST_H_
>> +#define __LIBCAMERA_V4L2_DEVICE_TEST_H_
>> +
>> +using namespace libcamera;
>> +
>> +class V4L2DeviceTest : public Test
>> +{
>> +public:
>> +	V4L2DeviceTest();
>> +	virtual ~V4L2DeviceTest() {};
>> +
>> +protected:
>> +	int init();
>> +	void cleanup();
>> +
>> +	virtual int run() = 0;
>> +
>> +	V4L2Device *dev;
>> +};
>> +
>> +#endif /* __LIBCAMERA_V4L2_DEVICE_TEST_H_ */
>> --
>> 2.17.1
>>
>> _______________________________________________
>> libcamera-devel mailing list
>> libcamera-devel at lists.libcamera.org
>> https://lists.libcamera.org/listinfo/libcamera-devel

-- 
Regards
--
Kieran


More information about the libcamera-devel mailing list