[libcamera-devel,1/5] cam: Add a parser for capture scripts
diff mbox series

Message ID 20220518171921.244168-2-jacopo@jmondi.org
State Accepted
Headers show
Series
  • cam: Add support for capture scripts
Related show

Commit Message

Jacopo Mondi May 18, 2022, 5:19 p.m. UTC
Add a parser class to the cam test application to control the capture
operations through a yaml script.

The capture script currently allow to specify a list of controls and
their associated values to be applied per-frame.

Also add a trivial capture script example to showcase the intended
script structure.

Signed-off-by: Jacopo Mondi <jacopo@jmondi.org>
---
 src/cam/capture-script.yaml |  44 ++++
 src/cam/capture_script.cpp  | 447 ++++++++++++++++++++++++++++++++++++
 src/cam/capture_script.h    |  57 +++++
 src/cam/meson.build         |   2 +
 4 files changed, 550 insertions(+)
 create mode 100644 src/cam/capture-script.yaml
 create mode 100644 src/cam/capture_script.cpp
 create mode 100644 src/cam/capture_script.h

Patch
diff mbox series

diff --git a/src/cam/capture-script.yaml b/src/cam/capture-script.yaml
new file mode 100644
index 000000000000..48eac5fedd46
--- /dev/null
+++ b/src/cam/capture-script.yaml
@@ -0,0 +1,44 @@ 
+# Capture scrip example
+#
+# A capture script allows to associate a list of controls and their values
+# to frame numbers.
+
+# \todo Formally define the capture script structure with a schema
+
+# Notes:
+# - Controls have to be specified by name, as defined in the
+#   libcamera::controls:: enumeration
+# - Controls not supported by the camera currently operated are ignored
+# - Frame numbers shall be monotonically incrementing, gaps are allowed
+
+# Example:
+frames:
+  - 1:
+      Brightness: 0.0
+
+  - 40:
+      Brightness: 0.2
+
+  - 80:
+      Brightness: 0.4
+
+  - 120:
+      Brightness: 0.8
+
+  - 160:
+      Brightness: 0.4
+
+  - 200:
+      Brightness: 0.2
+
+  - 240:
+      Brightness: 0.0
+
+  - 280:
+      Brightness: -0.2
+
+  - 300:
+      Brightness: -0.4
+
+  - 340:
+      Brightness: -0.8
diff --git a/src/cam/capture_script.cpp b/src/cam/capture_script.cpp
new file mode 100644
index 000000000000..d153dac7b0de
--- /dev/null
+++ b/src/cam/capture_script.cpp
@@ -0,0 +1,447 @@ 
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * capture_script.cpp - Capture session configuration script
+ */
+
+#include "capture_script.h"
+
+#include <iostream>
+#include <stdio.h>
+#include <stdlib.h>
+
+using namespace libcamera;
+
+namespace {
+
+/*
+ * Schema error debug function
+ * From: https://www.wpsoftware.net/andrew/pages/libyaml.html
+ */
+void printToken(yaml_token_t &token)
+{
+	std::string type;
+
+	switch (token.type) {
+	case YAML_KEY_TOKEN:
+		printf("(Key token)   ");
+		break;
+	case YAML_VALUE_TOKEN:
+		printf("(Value token) ");
+		break;
+	case YAML_BLOCK_SEQUENCE_START_TOKEN:
+		puts("<b>Start Block (Sequence)</b>");
+		break;
+	case YAML_BLOCK_ENTRY_TOKEN:
+		puts("<b>Start Block (Entry)</b>");
+		break;
+	case YAML_BLOCK_END_TOKEN:
+		puts("<b>End block</b>");
+		break;
+	case YAML_BLOCK_MAPPING_START_TOKEN:
+		puts("[Block mapping]");
+		break;
+	case YAML_SCALAR_TOKEN:
+		printf("scalar %s \n", token.data.scalar.value);
+		break;
+	default:
+		printf("Token type %u \n", token.type);
+		break;
+	}
+	std::cerr << std::endl;
+}
+
+} /* namespace */
+
+CaptureScript::CaptureScript(std::shared_ptr<Camera> camera, std::string fileName)
+	: camera_(camera), scriptPath_(fileName), valid_(false)
+{
+	if (!std::filesystem::is_regular_file(scriptPath_)) {
+		std::cerr << "Capture script file not found: "
+			  << scriptPath_ << std::endl;
+		return;
+	}
+
+	FILE *fh = fopen(scriptPath_.c_str(), "r");
+	if (!fh) {
+		int ret = -errno;
+		std::cerr << "Failed to open capture script " << scriptPath_
+			  << ": " << strerror(-ret) << std::endl;
+		return;
+	}
+
+	/*
+	 * Map the camera's controls to their name so that they can be
+	 * easily identified when parsing the script file.
+	 */
+	for (const auto &[control, info] : camera_->controls())
+		controls_[control->name()] = control;
+
+	int ret = parseScript(fh);
+	fclose(fh);
+	if (ret)
+		return;
+
+	valid_ = true;
+}
+
+/* Retrieve the control list associated with a frame number. */
+const ControlList &CaptureScript::frameControls(unsigned int frame)
+{
+	static ControlList controls{};
+
+	auto it = frameControls_.find(frame);
+	if (it == frameControls_.end())
+		return controls;
+
+	return it->second;
+}
+
+int CaptureScript::parseScript(FILE *script)
+{
+	int ret = yaml_parser_initialize(&parser_);
+	if (!ret) {
+		std::cerr << "Failed to initialize yaml parser" << std::endl;
+		return ret;
+	}
+
+	yaml_parser_set_input_file(&parser_, script);
+
+	yaml_token_t token;
+	yaml_parser_scan(&parser_, &token);
+	if (token.type != YAML_STREAM_START_TOKEN) {
+		std::cerr << "Configuration file not valid" << std::endl;
+		printToken(token);
+		yaml_token_delete(&token);
+		yaml_parser_delete(&parser_);
+		return -EINVAL;
+	}
+	yaml_token_delete(&token);
+
+	yaml_parser_scan(&parser_, &token);
+	if (token.type != YAML_BLOCK_MAPPING_START_TOKEN) {
+		std::cerr << "Configuration file not valid" << std::endl;
+		printToken(token);
+		yaml_token_delete(&token);
+		yaml_parser_delete(&parser_);
+		return -EINVAL;
+	}
+	yaml_token_delete(&token);
+
+	/* Capture script shall start with the "frames" key. */
+	std::string section = parseKey();
+	if (section != "frames") {
+		std::cerr << "Unsupported section: " << section << std::endl;
+		yaml_token_delete(&token);
+		yaml_parser_delete(&parser_);
+		return -EINVAL;
+	}
+
+	yaml_parser_scan(&parser_, &token);
+	if (token.type != YAML_VALUE_TOKEN) {
+		std::cerr << "Configuration file not valid" << std::endl;
+		printToken(token);
+		yaml_token_delete(&token);
+		yaml_parser_delete(&parser_);
+		return -EINVAL;
+	}
+	yaml_token_delete(&token);
+
+	parseFrames();
+
+	/* Consume the tokens on the stack until we don't hit an STREAM_END */
+	do {
+		ret = 0;
+
+		yaml_parser_scan(&parser_, &token);
+		switch (token.type) {
+		case YAML_STREAM_END_TOKEN:
+			ret = -ENOENT;
+			[[fallthrough]];
+		default:
+			yaml_token_delete(&token);
+			break;
+		}
+	} while (ret >= 0);
+
+	return 0;
+}
+
+int CaptureScript::parseFrames()
+{
+	int ret;
+
+	yaml_token_t token;
+	yaml_parser_scan(&parser_, &token);
+	if (token.type != YAML_BLOCK_SEQUENCE_START_TOKEN) {
+		std::cerr << "Configuration file not valid" << std::endl;
+		printToken(token);
+		yaml_token_delete(&token);
+		yaml_parser_delete(&parser_);
+		return -EINVAL;
+	}
+	yaml_token_delete(&token);
+
+	do {
+		yaml_parser_scan(&parser_, &token);
+		switch (token.type) {
+		case YAML_BLOCK_ENTRY_TOKEN:
+			ret = parseFrame();
+			yaml_token_delete(&token);
+			break;
+		case YAML_BLOCK_END_TOKEN:
+			ret = -ENODEV;
+			yaml_token_delete(&token);
+			break;
+		default:
+			ret = -EINVAL;
+			std::cerr << "Configuration file not valid" << std::endl;
+			printToken(token);
+			yaml_token_delete(&token);
+			yaml_parser_delete(&parser_);
+		}
+	} while (ret == 0);
+
+	return ret != -ENODEV ? ret : 0;
+}
+
+int CaptureScript::parseFrame()
+{
+	yaml_token_t token;
+
+	yaml_parser_scan(&parser_, &token);
+	if (token.type != YAML_BLOCK_MAPPING_START_TOKEN) {
+		std::cerr << "Configuration file not valid" << std::endl;
+		printToken(token);
+		yaml_token_delete(&token);
+		yaml_parser_delete(&parser_);
+		return -EINVAL;
+	}
+	yaml_token_delete(&token);
+
+	std::string key = parseKey();
+	unsigned int frameId = atoi(key.c_str());
+
+	frameControls_[frameId] = parseControls();
+
+	return 0;
+}
+
+ControlList CaptureScript::parseControls()
+{
+	ControlList controls{};
+	yaml_token_t token;
+
+	yaml_parser_scan(&parser_, &token);
+	if (token.type != YAML_VALUE_TOKEN) {
+		std::cerr << "Configuration file not valid" << std::endl;
+		printToken(token);
+		yaml_token_delete(&token);
+		yaml_parser_delete(&parser_);
+		return controls;
+	}
+	yaml_token_delete(&token);
+
+	/*
+	 * Whenever we get an unbalanced block_end token, we're done
+	 * parsing the controls.
+	 */
+	int blockCounter = 0;
+
+	do {
+		yaml_parser_scan(&parser_, &token);
+
+		switch (token.type) {
+		case YAML_BLOCK_MAPPING_START_TOKEN:
+			blockCounter++;
+			yaml_token_delete(&token);
+			break;
+		case YAML_BLOCK_END_TOKEN:
+			blockCounter--;
+			yaml_token_delete(&token);
+			break;
+		case YAML_KEY_TOKEN: {
+			/* We expect a value after a key. */
+			std::string controlName = parseKey(token);
+			std::string controlRepr = parseValue();
+
+			/*
+			 * If the camera does not support the control just
+			 * ignore it.
+			 */
+			auto controlIt = controls_.find(controlName);
+			if (controlIt == controls_.end()) {
+				std::cerr << "Unsupported control: "
+					  << controlName << std::endl;
+				break;
+			}
+
+			const ControlId *controlId = controlIt->second;
+			ControlValue val = unpackControl(controlId, controlRepr);
+			controls.set(controlId->id(), val);
+			break;
+		}
+		default:
+			/* Error condition */
+			yaml_token_delete(&token);
+			break;
+		}
+	} while (blockCounter >= 0);
+
+	return controls;
+}
+
+std::string CaptureScript::parseKey(yaml_token_t &token)
+{
+	/* Make sure the token type is a key and get its value. */
+	if (token.type != YAML_KEY_TOKEN) {
+		std::cerr << "Configuration file not valid" << std::endl;
+		printToken(token);
+		yaml_token_delete(&token);
+		yaml_parser_delete(&parser_);
+		return "";
+	}
+	yaml_token_delete(&token);
+
+	yaml_parser_scan(&parser_, &token);
+	if (token.type != YAML_SCALAR_TOKEN) {
+		yaml_token_delete(&token);
+		return "";
+	}
+
+	std::string value(reinterpret_cast<char *>(token.data.scalar.value),
+			  token.data.scalar.length);
+	yaml_token_delete(&token);
+
+	return value;
+}
+
+std::string CaptureScript::parseKey()
+{
+	yaml_token_t token;
+
+	yaml_parser_scan(&parser_, &token);
+	return parseKey(token);
+}
+
+std::string CaptureScript::parseValue()
+{
+	yaml_token_t token;
+
+	/* Make sure the token type is a value and get its content. */
+	yaml_parser_scan(&parser_, &token);
+	if (token.type != YAML_VALUE_TOKEN) {
+		std::cerr << "Configuration file not valid" << std::endl;
+		printToken(token);
+		yaml_token_delete(&token);
+		yaml_parser_delete(&parser_);
+		return "";
+	}
+	yaml_token_delete(&token);
+
+	yaml_parser_scan(&parser_, &token);
+	if (token.type != YAML_SCALAR_TOKEN) {
+		yaml_token_delete(&token);
+		return "";
+	}
+
+	std::string value(reinterpret_cast<char *>(token.data.scalar.value),
+			  token.data.scalar.length);
+	yaml_token_delete(&token);
+
+	return value;
+}
+
+void CaptureScript::unpackFailure(const ControlId *id, std::string repr)
+{
+	std::string ctrlType;
+
+	switch (id->type()) {
+	case ControlTypeNone:
+		ctrlType = "none";
+		break;
+	case ControlTypeBool:
+		ctrlType = "bool";
+		break;
+	case ControlTypeByte:
+		ctrlType = "byte";
+		break;
+	case ControlTypeInteger32:
+		ctrlType = "int32";
+		break;
+	case ControlTypeInteger64:
+		ctrlType = "int64";
+		break;
+	case ControlTypeFloat:
+		ctrlType = "float";
+		break;
+	case ControlTypeString:
+		ctrlType = "string";
+		break;
+	case ControlTypeRectangle:
+		ctrlType = "Rectangle";
+		break;
+	case ControlTypeSize:
+		ctrlType = "Size";
+		break;
+	}
+
+	std::cerr << "Unsupported control \'" << repr <<"\'" << " for "
+		  << ctrlType << " control " << id->name() << std::endl;
+}
+
+ControlValue CaptureScript::unpackControl(const ControlId *id, std::string repr)
+{
+	ControlValue value{};
+
+	switch (id->type()) {
+	case ControlTypeNone:
+		break;
+	case ControlTypeBool: {
+		bool val;
+
+		if (repr == "true") {
+			val = true;
+		} else if (repr == "false") {
+			val = false;
+		} else {
+			unpackFailure(id, repr);
+			return value;
+		}
+
+		value.set<bool>(val);
+		break;
+	}
+	case ControlTypeByte: {
+		uint8_t val = strtol(repr.c_str(), NULL, 10);
+		value.set<uint8_t>(val);
+		break;
+	}
+	case ControlTypeInteger32: {
+		int32_t val = strtol(repr.c_str(), NULL, 10);
+		value.set<int32_t>(val);
+		break;
+	}
+	case ControlTypeInteger64: {
+		int64_t val = strtoll(repr.c_str(), NULL, 10);
+		value.set<int64_t>(val);
+		break;
+	}
+	case ControlTypeFloat: {
+		float val = strtof(repr.c_str(), NULL);
+		value.set<float>(val);
+		break;
+	}
+	case ControlTypeString: {
+		value.set<std::string>(repr);
+		break;
+	}
+	case ControlTypeRectangle:
+		/* \todo Parse rectangles. */
+		break;
+	case ControlTypeSize:
+		/* \todo Parse Sizes. */
+		break;
+	}
+
+	return value;
+}
diff --git a/src/cam/capture_script.h b/src/cam/capture_script.h
new file mode 100644
index 000000000000..39fded1250a6
--- /dev/null
+++ b/src/cam/capture_script.h
@@ -0,0 +1,57 @@ 
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * capture_script.h - Capture session configuration script
+ */
+
+#pragma once
+
+#if defined(_GLIBCXX_RELEASE) && _GLIBCXX_RELEASE < 8
+#include <experimental/filesystem>
+namespace std {
+namespace filesystem = std::experimental::filesystem;
+}
+#else
+#include <filesystem>
+#endif
+
+#include <map>
+#include <memory>
+#include <string>
+
+#include <libcamera/camera.h>
+#include <libcamera/controls.h>
+
+#include <yaml.h>
+
+class CaptureScript
+{
+public:
+	CaptureScript(std::shared_ptr<libcamera::Camera> camera,
+		      std::string fileName);
+
+	bool valid() const { return valid_; }
+
+	const libcamera::ControlList &frameControls(unsigned int frame);
+
+private:
+	std::map<std::string, const libcamera::ControlId *> controls_;
+	std::map<unsigned int, libcamera::ControlList> frameControls_;
+	std::shared_ptr<libcamera::Camera> camera_;
+	std::filesystem::path scriptPath_;
+	yaml_parser_t parser_;
+	bool valid_;
+
+	int parseScript(FILE *script);
+
+	libcamera::ControlList parseControls();
+	int parseFrames();
+	int parseFrame();
+
+	std::string parseKey(yaml_token_t &token);
+	std::string parseKey();
+	std::string parseValue();
+
+	void unpackFailure(const libcamera::ControlId *id, std::string repr);
+	libcamera::ControlValue unpackControl(const libcamera::ControlId *id,
+					      std::string repr);
+};
diff --git a/src/cam/meson.build b/src/cam/meson.build
index afc0ea9f397a..60ecbdca0c38 100644
--- a/src/cam/meson.build
+++ b/src/cam/meson.build
@@ -11,6 +11,7 @@  cam_enabled = true
 
 cam_sources = files([
     'camera_session.cpp',
+    'capture_script.cpp',
     'event_loop.cpp',
     'file_sink.cpp',
     'frame_sink.cpp',
@@ -59,6 +60,7 @@  cam  = executable('cam', cam_sources,
                       libsdl2,
                       libsdl2_image,
                       libevent,
+                      libyaml,
                   ],
                   cpp_args : cam_cpp_args,
                   install : true)