Import libgpiod_2.1.3.orig.tar.xz
authorGavin Lai (賴建宇) <gavin09@gmail.com>
Sun, 18 Aug 2024 08:17:42 +0000 (16:17 +0800)
committerGavin Lai (賴建宇) <gavin09@gmail.com>
Sun, 18 Aug 2024 08:17:42 +0000 (16:17 +0800)
[dgit import orig libgpiod_2.1.3.orig.tar.xz]

278 files changed:
.gitignore [new file with mode: 0644]
.readthedocs.yaml [new file with mode: 0644]
COPYING [new file with mode: 0644]
Doxyfile.in [new file with mode: 0644]
LICENSES/Apache-2.0.txt [new file with mode: 0644]
LICENSES/BSD-3-Clause.txt [new file with mode: 0644]
LICENSES/CC-BY-SA-4.0.txt [new file with mode: 0644]
LICENSES/CC0-1.0.txt [new file with mode: 0644]
LICENSES/GPL-2.0-only.txt [new file with mode: 0644]
LICENSES/GPL-2.0-or-later.txt [new symlink]
LICENSES/LGPL-2.1-or-later.txt [new file with mode: 0644]
LICENSES/Linux-syscall-note.txt [new file with mode: 0644]
Makefile.am [new file with mode: 0644]
NEWS [new file with mode: 0644]
README [new file with mode: 0644]
TODO [new file with mode: 0644]
autogen.sh [new file with mode: 0755]
bindings/Makefile.am [new file with mode: 0644]
bindings/cxx/Makefile.am [new file with mode: 0644]
bindings/cxx/chip-info.cpp [new file with mode: 0644]
bindings/cxx/chip.cpp [new file with mode: 0644]
bindings/cxx/edge-event-buffer.cpp [new file with mode: 0644]
bindings/cxx/edge-event.cpp [new file with mode: 0644]
bindings/cxx/examples/.gitignore [new file with mode: 0644]
bindings/cxx/examples/Makefile.am [new file with mode: 0644]
bindings/cxx/examples/async_watch_line_value.cpp [new file with mode: 0644]
bindings/cxx/examples/find_line_by_name.cpp [new file with mode: 0644]
bindings/cxx/examples/get_chip_info.cpp [new file with mode: 0644]
bindings/cxx/examples/get_line_info.cpp [new file with mode: 0644]
bindings/cxx/examples/get_line_value.cpp [new file with mode: 0644]
bindings/cxx/examples/get_multiple_line_values.cpp [new file with mode: 0644]
bindings/cxx/examples/reconfigure_input_to_output.cpp [new file with mode: 0644]
bindings/cxx/examples/toggle_line_value.cpp [new file with mode: 0644]
bindings/cxx/examples/toggle_multiple_line_values.cpp [new file with mode: 0644]
bindings/cxx/examples/watch_line_info.cpp [new file with mode: 0644]
bindings/cxx/examples/watch_line_rising.cpp [new file with mode: 0644]
bindings/cxx/examples/watch_line_value.cpp [new file with mode: 0644]
bindings/cxx/examples/watch_multiple_line_values.cpp [new file with mode: 0644]
bindings/cxx/exception.cpp [new file with mode: 0644]
bindings/cxx/gpiod.hpp [new file with mode: 0644]
bindings/cxx/gpiodcxx/Makefile.am [new file with mode: 0644]
bindings/cxx/gpiodcxx/chip-info.hpp [new file with mode: 0644]
bindings/cxx/gpiodcxx/chip.hpp [new file with mode: 0644]
bindings/cxx/gpiodcxx/edge-event-buffer.hpp [new file with mode: 0644]
bindings/cxx/gpiodcxx/edge-event.hpp [new file with mode: 0644]
bindings/cxx/gpiodcxx/exception.hpp [new file with mode: 0644]
bindings/cxx/gpiodcxx/info-event.hpp [new file with mode: 0644]
bindings/cxx/gpiodcxx/line-config.hpp [new file with mode: 0644]
bindings/cxx/gpiodcxx/line-info.hpp [new file with mode: 0644]
bindings/cxx/gpiodcxx/line-request.hpp [new file with mode: 0644]
bindings/cxx/gpiodcxx/line-settings.hpp [new file with mode: 0644]
bindings/cxx/gpiodcxx/line.hpp [new file with mode: 0644]
bindings/cxx/gpiodcxx/misc.hpp [new file with mode: 0644]
bindings/cxx/gpiodcxx/request-builder.hpp [new file with mode: 0644]
bindings/cxx/gpiodcxx/request-config.hpp [new file with mode: 0644]
bindings/cxx/gpiodcxx/timestamp.hpp [new file with mode: 0644]
bindings/cxx/info-event.cpp [new file with mode: 0644]
bindings/cxx/internal.cpp [new file with mode: 0644]
bindings/cxx/internal.hpp [new file with mode: 0644]
bindings/cxx/libgpiodcxx.pc.in [new file with mode: 0644]
bindings/cxx/line-config.cpp [new file with mode: 0644]
bindings/cxx/line-info.cpp [new file with mode: 0644]
bindings/cxx/line-request.cpp [new file with mode: 0644]
bindings/cxx/line-settings.cpp [new file with mode: 0644]
bindings/cxx/line.cpp [new file with mode: 0644]
bindings/cxx/misc.cpp [new file with mode: 0644]
bindings/cxx/request-builder.cpp [new file with mode: 0644]
bindings/cxx/request-config.cpp [new file with mode: 0644]
bindings/cxx/tests/.gitignore [new file with mode: 0644]
bindings/cxx/tests/Makefile.am [new file with mode: 0644]
bindings/cxx/tests/check-kernel.cpp [new file with mode: 0644]
bindings/cxx/tests/gpiod-cxx-test-main.cpp [new file with mode: 0644]
bindings/cxx/tests/gpiosim.cpp [new file with mode: 0644]
bindings/cxx/tests/gpiosim.hpp [new file with mode: 0644]
bindings/cxx/tests/helpers.cpp [new file with mode: 0644]
bindings/cxx/tests/helpers.hpp [new file with mode: 0644]
bindings/cxx/tests/tests-chip-info.cpp [new file with mode: 0644]
bindings/cxx/tests/tests-chip.cpp [new file with mode: 0644]
bindings/cxx/tests/tests-edge-event.cpp [new file with mode: 0644]
bindings/cxx/tests/tests-info-event.cpp [new file with mode: 0644]
bindings/cxx/tests/tests-line-config.cpp [new file with mode: 0644]
bindings/cxx/tests/tests-line-info.cpp [new file with mode: 0644]
bindings/cxx/tests/tests-line-request.cpp [new file with mode: 0644]
bindings/cxx/tests/tests-line-settings.cpp [new file with mode: 0644]
bindings/cxx/tests/tests-line.cpp [new file with mode: 0644]
bindings/cxx/tests/tests-misc.cpp [new file with mode: 0644]
bindings/cxx/tests/tests-request-config.cpp [new file with mode: 0644]
bindings/python/.gitignore [new file with mode: 0644]
bindings/python/MANIFEST.in [new file with mode: 0644]
bindings/python/Makefile.am [new file with mode: 0644]
bindings/python/README.md [new file with mode: 0644]
bindings/python/examples/Makefile.am [new file with mode: 0644]
bindings/python/examples/async_watch_line_value.py [new file with mode: 0755]
bindings/python/examples/find_line_by_name.py [new file with mode: 0755]
bindings/python/examples/get_chip_info.py [new file with mode: 0755]
bindings/python/examples/get_line_info.py [new file with mode: 0755]
bindings/python/examples/get_line_value.py [new file with mode: 0755]
bindings/python/examples/get_multiple_line_values.py [new file with mode: 0755]
bindings/python/examples/reconfigure_input_to_output.py [new file with mode: 0755]
bindings/python/examples/toggle_line_value.py [new file with mode: 0755]
bindings/python/examples/toggle_multiple_line_values.py [new file with mode: 0755]
bindings/python/examples/watch_line_info.py [new file with mode: 0755]
bindings/python/examples/watch_line_rising.py [new file with mode: 0755]
bindings/python/examples/watch_line_value.py [new file with mode: 0755]
bindings/python/examples/watch_multiple_line_values.py [new file with mode: 0755]
bindings/python/gpiod/Makefile.am [new file with mode: 0644]
bindings/python/gpiod/__init__.py [new file with mode: 0644]
bindings/python/gpiod/chip.py [new file with mode: 0644]
bindings/python/gpiod/chip_info.py [new file with mode: 0644]
bindings/python/gpiod/edge_event.py [new file with mode: 0644]
bindings/python/gpiod/exception.py [new file with mode: 0644]
bindings/python/gpiod/ext/Makefile.am [new file with mode: 0644]
bindings/python/gpiod/ext/chip.c [new file with mode: 0644]
bindings/python/gpiod/ext/common.c [new file with mode: 0644]
bindings/python/gpiod/ext/internal.h [new file with mode: 0644]
bindings/python/gpiod/ext/line-config.c [new file with mode: 0644]
bindings/python/gpiod/ext/line-settings.c [new file with mode: 0644]
bindings/python/gpiod/ext/module.c [new file with mode: 0644]
bindings/python/gpiod/ext/request.c [new file with mode: 0644]
bindings/python/gpiod/info_event.py [new file with mode: 0644]
bindings/python/gpiod/internal.py [new file with mode: 0644]
bindings/python/gpiod/line.py [new file with mode: 0644]
bindings/python/gpiod/line_info.py [new file with mode: 0644]
bindings/python/gpiod/line_request.py [new file with mode: 0644]
bindings/python/gpiod/line_settings.py [new file with mode: 0644]
bindings/python/gpiod/version.py [new file with mode: 0644]
bindings/python/pyproject.toml [new file with mode: 0644]
bindings/python/setup.py [new file with mode: 0644]
bindings/python/tests/Makefile.am [new file with mode: 0644]
bindings/python/tests/__init__.py [new file with mode: 0644]
bindings/python/tests/__main__.py [new file with mode: 0644]
bindings/python/tests/gpiosim/Makefile.am [new file with mode: 0644]
bindings/python/tests/gpiosim/__init__.py [new file with mode: 0644]
bindings/python/tests/gpiosim/chip.py [new file with mode: 0644]
bindings/python/tests/gpiosim/ext.c [new file with mode: 0644]
bindings/python/tests/helpers.py [new file with mode: 0644]
bindings/python/tests/procname/Makefile.am [new file with mode: 0644]
bindings/python/tests/procname/__init__.py [new file with mode: 0644]
bindings/python/tests/procname/ext.c [new file with mode: 0644]
bindings/python/tests/tests_chip.py [new file with mode: 0644]
bindings/python/tests/tests_chip_info.py [new file with mode: 0644]
bindings/python/tests/tests_edge_event.py [new file with mode: 0644]
bindings/python/tests/tests_info_event.py [new file with mode: 0644]
bindings/python/tests/tests_line_info.py [new file with mode: 0644]
bindings/python/tests/tests_line_request.py [new file with mode: 0644]
bindings/python/tests/tests_line_settings.py [new file with mode: 0644]
bindings/python/tests/tests_module.py [new file with mode: 0644]
bindings/rust/.gitignore [new file with mode: 0644]
bindings/rust/Cargo.toml [new file with mode: 0644]
bindings/rust/Makefile.am [new file with mode: 0644]
bindings/rust/gpiosim-sys/Cargo.toml [new file with mode: 0644]
bindings/rust/gpiosim-sys/Makefile.am [new file with mode: 0644]
bindings/rust/gpiosim-sys/README.md [new file with mode: 0644]
bindings/rust/gpiosim-sys/build.rs [new file with mode: 0644]
bindings/rust/gpiosim-sys/src/Makefile.am [new file with mode: 0644]
bindings/rust/gpiosim-sys/src/lib.rs [new file with mode: 0644]
bindings/rust/gpiosim-sys/src/sim.rs [new file with mode: 0644]
bindings/rust/libgpiod-sys/Cargo.toml [new file with mode: 0644]
bindings/rust/libgpiod-sys/Makefile.am [new file with mode: 0644]
bindings/rust/libgpiod-sys/README.md [new file with mode: 0644]
bindings/rust/libgpiod-sys/build.rs [new file with mode: 0644]
bindings/rust/libgpiod-sys/src/Makefile.am [new file with mode: 0644]
bindings/rust/libgpiod-sys/src/lib.rs [new file with mode: 0644]
bindings/rust/libgpiod-sys/wrapper.h [new file with mode: 0644]
bindings/rust/libgpiod/Cargo.toml [new file with mode: 0644]
bindings/rust/libgpiod/Makefile.am [new file with mode: 0644]
bindings/rust/libgpiod/README.md [new file with mode: 0644]
bindings/rust/libgpiod/examples/Makefile.am [new file with mode: 0644]
bindings/rust/libgpiod/examples/buffered_event_lifetimes.rs [new file with mode: 0644]
bindings/rust/libgpiod/examples/find_line_by_name.rs [new file with mode: 0644]
bindings/rust/libgpiod/examples/get_chip_info.rs [new file with mode: 0644]
bindings/rust/libgpiod/examples/get_line_info.rs [new file with mode: 0644]
bindings/rust/libgpiod/examples/get_line_value.rs [new file with mode: 0644]
bindings/rust/libgpiod/examples/get_multiple_line_values.rs [new file with mode: 0644]
bindings/rust/libgpiod/examples/reconfigure_input_to_output.rs [new file with mode: 0644]
bindings/rust/libgpiod/examples/toggle_line_value.rs [new file with mode: 0644]
bindings/rust/libgpiod/examples/toggle_multiple_line_values.rs [new file with mode: 0644]
bindings/rust/libgpiod/examples/watch_line_info.rs [new file with mode: 0644]
bindings/rust/libgpiod/examples/watch_line_rising.rs [new file with mode: 0644]
bindings/rust/libgpiod/examples/watch_line_value.rs [new file with mode: 0644]
bindings/rust/libgpiod/examples/watch_multiple_line_values.rs [new file with mode: 0644]
bindings/rust/libgpiod/src/Makefile.am [new file with mode: 0644]
bindings/rust/libgpiod/src/chip.rs [new file with mode: 0644]
bindings/rust/libgpiod/src/edge_event.rs [new file with mode: 0644]
bindings/rust/libgpiod/src/event_buffer.rs [new file with mode: 0644]
bindings/rust/libgpiod/src/info_event.rs [new file with mode: 0644]
bindings/rust/libgpiod/src/lib.rs [new file with mode: 0644]
bindings/rust/libgpiod/src/line_config.rs [new file with mode: 0644]
bindings/rust/libgpiod/src/line_info.rs [new file with mode: 0644]
bindings/rust/libgpiod/src/line_request.rs [new file with mode: 0644]
bindings/rust/libgpiod/src/line_settings.rs [new file with mode: 0644]
bindings/rust/libgpiod/src/request_config.rs [new file with mode: 0644]
bindings/rust/libgpiod/tests/Makefile.am [new file with mode: 0644]
bindings/rust/libgpiod/tests/chip.rs [new file with mode: 0644]
bindings/rust/libgpiod/tests/common/Makefile.am [new file with mode: 0644]
bindings/rust/libgpiod/tests/common/config.rs [new file with mode: 0644]
bindings/rust/libgpiod/tests/common/mod.rs [new file with mode: 0644]
bindings/rust/libgpiod/tests/edge_event.rs [new file with mode: 0644]
bindings/rust/libgpiod/tests/info_event.rs [new file with mode: 0644]
bindings/rust/libgpiod/tests/line_config.rs [new file with mode: 0644]
bindings/rust/libgpiod/tests/line_info.rs [new file with mode: 0644]
bindings/rust/libgpiod/tests/line_request.rs [new file with mode: 0644]
bindings/rust/libgpiod/tests/line_settings.rs [new file with mode: 0644]
bindings/rust/libgpiod/tests/request_config.rs [new file with mode: 0644]
configure.ac [new file with mode: 0644]
contrib/Android.bp [new file with mode: 0644]
contrib/Makefile.am [new file with mode: 0644]
examples/.gitignore [new file with mode: 0644]
examples/Makefile.am [new file with mode: 0644]
examples/async_watch_line_value.c [new file with mode: 0644]
examples/find_line_by_name.c [new file with mode: 0644]
examples/get_chip_info.c [new file with mode: 0644]
examples/get_line_info.c [new file with mode: 0644]
examples/get_line_value.c [new file with mode: 0644]
examples/get_multiple_line_values.c [new file with mode: 0644]
examples/reconfigure_input_to_output.c [new file with mode: 0644]
examples/toggle_line_value.c [new file with mode: 0644]
examples/toggle_multiple_line_values.c [new file with mode: 0644]
examples/watch_line_info.c [new file with mode: 0644]
examples/watch_line_rising.c [new file with mode: 0644]
examples/watch_line_value.c [new file with mode: 0644]
examples/watch_multiple_line_values.c [new file with mode: 0644]
include/Makefile.am [new file with mode: 0644]
include/gpiod.h [new file with mode: 0644]
lib/Makefile.am [new file with mode: 0644]
lib/chip-info.c [new file with mode: 0644]
lib/chip.c [new file with mode: 0644]
lib/edge-event.c [new file with mode: 0644]
lib/info-event.c [new file with mode: 0644]
lib/internal.c [new file with mode: 0644]
lib/internal.h [new file with mode: 0644]
lib/libgpiod.pc.in [new file with mode: 0644]
lib/line-config.c [new file with mode: 0644]
lib/line-info.c [new file with mode: 0644]
lib/line-request.c [new file with mode: 0644]
lib/line-settings.c [new file with mode: 0644]
lib/misc.c [new file with mode: 0644]
lib/request-config.c [new file with mode: 0644]
lib/uapi/gpio.h [new file with mode: 0644]
man/.gitignore [new file with mode: 0644]
man/Makefile.am [new file with mode: 0644]
man/template [new file with mode: 0644]
sphinx/conf.py [new file with mode: 0644]
sphinx/index.rst [new file with mode: 0644]
tests/.gitignore [new file with mode: 0644]
tests/Makefile.am [new file with mode: 0644]
tests/gpiod-test-helpers.c [new file with mode: 0644]
tests/gpiod-test-helpers.h [new file with mode: 0644]
tests/gpiod-test-sim.c [new file with mode: 0644]
tests/gpiod-test-sim.h [new file with mode: 0644]
tests/gpiod-test.c [new file with mode: 0644]
tests/gpiod-test.h [new file with mode: 0644]
tests/gpiosim/.gitignore [new file with mode: 0644]
tests/gpiosim/Makefile.am [new file with mode: 0644]
tests/gpiosim/gpiosim-selftest.c [new file with mode: 0644]
tests/gpiosim/gpiosim.c [new file with mode: 0644]
tests/gpiosim/gpiosim.h [new file with mode: 0644]
tests/tests-chip-info.c [new file with mode: 0644]
tests/tests-chip.c [new file with mode: 0644]
tests/tests-edge-event.c [new file with mode: 0644]
tests/tests-info-event.c [new file with mode: 0644]
tests/tests-line-config.c [new file with mode: 0644]
tests/tests-line-info.c [new file with mode: 0644]
tests/tests-line-request.c [new file with mode: 0644]
tests/tests-line-settings.c [new file with mode: 0644]
tests/tests-misc.c [new file with mode: 0644]
tests/tests-request-config.c [new file with mode: 0644]
tools/.gitignore [new file with mode: 0644]
tools/Makefile.am [new file with mode: 0644]
tools/gpio-tools-test.bash [new file with mode: 0755]
tools/gpiodetect.c [new file with mode: 0644]
tools/gpioget.c [new file with mode: 0644]
tools/gpioinfo.c [new file with mode: 0644]
tools/gpiomon.c [new file with mode: 0644]
tools/gpionotify.c [new file with mode: 0644]
tools/gpioset.c [new file with mode: 0644]
tools/tools-common.c [new file with mode: 0644]
tools/tools-common.h [new file with mode: 0644]

diff --git a/.gitignore b/.gitignore
new file mode 100644 (file)
index 0000000..6c08415
--- /dev/null
@@ -0,0 +1,37 @@
+# SPDX-License-Identifier: GPL-2.0-or-later
+# SPDX-FileCopyrightText: 2017-2021 Bartosz Golaszewski <bartekgola@gmail.com>
+
+*.o
+*.lo
+*.la
+doc
+*.pc
+*.tar.gz
+*.patch
+*.swp
+tags
+
+# autotools stuff
+.deps/
+.libs/
+Doxyfile
+Makefile
+Makefile.in
+aclocal.m4
+autom4te.cache/
+autostuff/
+config.h
+config.h.in
+config.h.in~
+config.log
+config.status
+configure
+configure~
+libtool
+*-libtool
+m4/
+stamp-h1
+
+# profiling
+*.gcda
+*.gcno
diff --git a/.readthedocs.yaml b/.readthedocs.yaml
new file mode 100644 (file)
index 0000000..f40e95f
--- /dev/null
@@ -0,0 +1,27 @@
+# SPDX-License-Identifier: LGPL-2.1-or-later
+# SPDX-FileCopyrightText: 2022 Kent Gibson <warthog618@gmail.com>
+
+#
+# This file is part of libgpiod.
+#
+# Read the Docs configuration file
+# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
+
+version: 2
+
+build:
+  os: ubuntu-22.04
+  tools:
+    python: "3.11"
+  # doxygen is available by default, but just in case.
+  # others are definitely missing.
+  apt_packages:
+      - autoconf
+      - autoconf-archive
+      - libtool
+      - m4
+      - doxygen
+      - graphviz
+
+sphinx:
+   configuration: sphinx/conf.py
diff --git a/COPYING b/COPYING
new file mode 100644 (file)
index 0000000..84b4fc6
--- /dev/null
+++ b/COPYING
@@ -0,0 +1,41 @@
+libgpiod is provided under:
+
+    SPDX-License-Identifier: LGPL-2.1-or-later
+
+Being under the terms of the GNU Lesser General Public License version 2.1
+or any later version according with:
+
+    LICENSES/LGPL-2.1-or-later.txt
+
+gpio-tools, test suites and examples are provided under:
+
+    SPDX-License-Identifier: GPL-2.0-or-later
+
+Being under the terms of the GNU General Public License version 2.0 or any
+later version according with:
+
+    LICENSES/GPL-2.0-or-later.txt
+
+The Linux Kernel uAPI headers are provided under:
+
+    SPDX-License-Identifier: GPL-2.0-only WITH Linux-syscall-note
+
+Being under the terms of the GNU General Public License version 2 only,
+according with:
+
+    LICENSES/GPL-2.0-only.txt
+
+With an explicit syscall exception, as stated at:
+
+    LICENSES/Linux-syscall-note.txt
+
+libgpiod text files are provided under:
+
+    SPDX-License-Identifier: CC-BY-SA-4.0
+
+Being under the terms of the Creative Commons Attribution-ShareAlike 4.0
+International License according with:
+
+    LICENSES/CC-BY-SA-4.0.txt
+
+All contributions to libgpiod are subject to this COPYING file.
diff --git a/Doxyfile.in b/Doxyfile.in
new file mode 100644 (file)
index 0000000..9c85e21
--- /dev/null
@@ -0,0 +1,105 @@
+# SPDX-License-Identifier: GPL-2.0-or-later
+# SPDX-FileCopyrightText: 2017-2021 Bartosz Golaszewski <bartekgola@gmail.com>
+
+# libgpiod doxygen configuration
+
+# General configuration
+PROJECT_NAME           = libgpiod
+PROJECT_NUMBER         = @VERSION_STR@
+OUTPUT_DIRECTORY       = doc
+OUTPUT_LANGUAGE        = English
+EXTRACT_ALL            = NO
+EXTRACT_PRIVATE        = NO
+EXTRACT_STATIC         = YES
+HIDE_UNDOC_MEMBERS     = NO
+HIDE_UNDOC_CLASSES     = NO
+BRIEF_MEMBER_DESC      = YES
+REPEAT_BRIEF           = YES
+ALWAYS_DETAILED_SEC    = NO
+FULL_PATH_NAMES        = NO
+STRIP_FROM_PATH        = @top_srcdir@
+INTERNAL_DOCS          = NO
+STRIP_CODE_COMMENTS    = YES
+CASE_SENSE_NAMES       = YES
+SHORT_NAMES            = NO
+HIDE_SCOPE_NAMES       = NO
+VERBATIM_HEADERS       = YES
+SHOW_INCLUDE_FILES     = YES
+JAVADOC_AUTOBRIEF      = YES
+INHERIT_DOCS           = YES
+INLINE_INFO            = YES
+SORT_MEMBER_DOCS       = YES
+DISTRIBUTE_GROUP_DOC   = NO
+TAB_SIZE               = 8
+GENERATE_TODOLIST      = YES
+GENERATE_TESTLIST      = YES
+GENERATE_BUGLIST       = YES
+ALIASES                =
+ENABLED_SECTIONS       =
+MAX_INITIALIZER_LINES  = 30
+OPTIMIZE_OUTPUT_FOR_C  = YES
+SHOW_USED_FILES        = YES
+QUIET                  = YES
+WARNINGS               = YES
+WARN_IF_UNDOCUMENTED   = YES
+WARN_FORMAT            =
+WARN_LOGFILE           =
+INPUT                  = @top_srcdir@/include/gpiod.h \
+                         @top_srcdir@/bindings/cxx/gpiod.hpp \
+                         @top_srcdir@/bindings/cxx/gpiodcxx/
+SOURCE_BROWSER         = YES
+INLINE_SOURCES         = NO
+REFERENCED_BY_RELATION = YES
+REFERENCES_RELATION    = YES
+ALPHABETICAL_INDEX     = NO
+IGNORE_PREFIX          =
+SEARCHENGINE           = NO
+ENABLE_PREPROCESSING   = YES
+
+# HTML output
+GENERATE_HTML          = YES
+HTML_OUTPUT            =
+HTML_HEADER            =
+HTML_FOOTER            =
+HTML_STYLESHEET        =
+GENERATE_HTMLHELP      = NO
+GENERATE_CHI           = NO
+BINARY_TOC             = NO
+TOC_EXPAND             = NO
+DISABLE_INDEX          = NO
+ENUM_VALUES_PER_LINE   = 4
+GENERATE_TREEVIEW      = NO
+TREEVIEW_WIDTH         = 250
+
+# LaTeX output
+GENERATE_LATEX         = NO
+LATEX_OUTPUT           =
+COMPACT_LATEX          = NO
+PAPER_TYPE             = a4
+EXTRA_PACKAGES         =
+LATEX_HEADER           =
+PDF_HYPERLINKS         = NO
+USE_PDFLATEX           = NO
+LATEX_BATCHMODE        = NO
+
+# RTF output
+GENERATE_RTF           = NO
+RTF_OUTPUT             =
+COMPACT_RTF            = NO
+RTF_HYPERLINKS         = NO
+RTF_STYLESHEET_FILE    =
+RTF_EXTENSIONS_FILE    =
+
+# Man page output
+GENERATE_MAN           = YES
+MAN_OUTPUT             = man
+MAN_EXTENSION          = .3
+MAN_LINKS              = YES
+
+# XML output
+GENERATE_XML           = YES
+
+# External references
+TAGFILES               =
+GENERATE_TAGFILE       =
+ALLEXTERNALS           = NO
diff --git a/LICENSES/Apache-2.0.txt b/LICENSES/Apache-2.0.txt
new file mode 100644 (file)
index 0000000..261eeb9
--- /dev/null
@@ -0,0 +1,201 @@
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright [yyyy] [name of copyright owner]
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
diff --git a/LICENSES/BSD-3-Clause.txt b/LICENSES/BSD-3-Clause.txt
new file mode 100644 (file)
index 0000000..ddd44f6
--- /dev/null
@@ -0,0 +1,28 @@
+BSD 3-Clause License
+
+Copyright (c) [year], [fullname]
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+1. Redistributions of source code must retain the above copyright notice, this
+   list of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright notice,
+   this list of conditions and the following disclaimer in the documentation
+   and/or other materials provided with the distribution.
+
+3. Neither the name of the copyright holder nor the names of its
+   contributors may be used to endorse or promote products derived from
+   this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/LICENSES/CC-BY-SA-4.0.txt b/LICENSES/CC-BY-SA-4.0.txt
new file mode 100644 (file)
index 0000000..3b7b82d
--- /dev/null
@@ -0,0 +1,427 @@
+Attribution-ShareAlike 4.0 International
+
+=======================================================================
+
+Creative Commons Corporation ("Creative Commons") is not a law firm and
+does not provide legal services or legal advice. Distribution of
+Creative Commons public licenses does not create a lawyer-client or
+other relationship. Creative Commons makes its licenses and related
+information available on an "as-is" basis. Creative Commons gives no
+warranties regarding its licenses, any material licensed under their
+terms and conditions, or any related information. Creative Commons
+disclaims all liability for damages resulting from their use to the
+fullest extent possible.
+
+Using Creative Commons Public Licenses
+
+Creative Commons public licenses provide a standard set of terms and
+conditions that creators and other rights holders may use to share
+original works of authorship and other material subject to copyright
+and certain other rights specified in the public license below. The
+following considerations are for informational purposes only, are not
+exhaustive, and do not form part of our licenses.
+
+     Considerations for licensors: Our public licenses are
+     intended for use by those authorized to give the public
+     permission to use material in ways otherwise restricted by
+     copyright and certain other rights. Our licenses are
+     irrevocable. Licensors should read and understand the terms
+     and conditions of the license they choose before applying it.
+     Licensors should also secure all rights necessary before
+     applying our licenses so that the public can reuse the
+     material as expected. Licensors should clearly mark any
+     material not subject to the license. This includes other CC-
+     licensed material, or material used under an exception or
+     limitation to copyright. More considerations for licensors:
+       wiki.creativecommons.org/Considerations_for_licensors
+
+     Considerations for the public: By using one of our public
+     licenses, a licensor grants the public permission to use the
+     licensed material under specified terms and conditions. If
+     the licensor's permission is not necessary for any reason--for
+     example, because of any applicable exception or limitation to
+     copyright--then that use is not regulated by the license. Our
+     licenses grant only permissions under copyright and certain
+     other rights that a licensor has authority to grant. Use of
+     the licensed material may still be restricted for other
+     reasons, including because others have copyright or other
+     rights in the material. A licensor may make special requests,
+     such as asking that all changes be marked or described.
+     Although not required by our licenses, you are encouraged to
+     respect those requests where reasonable. More_considerations
+     for the public:
+       wiki.creativecommons.org/Considerations_for_licensees
+
+=======================================================================
+
+Creative Commons Attribution-ShareAlike 4.0 International Public
+License
+
+By exercising the Licensed Rights (defined below), You accept and agree
+to be bound by the terms and conditions of this Creative Commons
+Attribution-ShareAlike 4.0 International Public License ("Public
+License"). To the extent this Public License may be interpreted as a
+contract, You are granted the Licensed Rights in consideration of Your
+acceptance of these terms and conditions, and the Licensor grants You
+such rights in consideration of benefits the Licensor receives from
+making the Licensed Material available under these terms and
+conditions.
+
+
+Section 1 -- Definitions.
+
+  a. Adapted Material means material subject to Copyright and Similar
+     Rights that is derived from or based upon the Licensed Material
+     and in which the Licensed Material is translated, altered,
+     arranged, transformed, or otherwise modified in a manner requiring
+     permission under the Copyright and Similar Rights held by the
+     Licensor. For purposes of this Public License, where the Licensed
+     Material is a musical work, performance, or sound recording,
+     Adapted Material is always produced where the Licensed Material is
+     synched in timed relation with a moving image.
+
+  b. Adapter's License means the license You apply to Your Copyright
+     and Similar Rights in Your contributions to Adapted Material in
+     accordance with the terms and conditions of this Public License.
+
+  c. BY-SA Compatible License means a license listed at
+     creativecommons.org/compatiblelicenses, approved by Creative
+     Commons as essentially the equivalent of this Public License.
+
+  d. Copyright and Similar Rights means copyright and/or similar rights
+     closely related to copyright including, without limitation,
+     performance, broadcast, sound recording, and Sui Generis Database
+     Rights, without regard to how the rights are labeled or
+     categorized. For purposes of this Public License, the rights
+     specified in Section 2(b)(1)-(2) are not Copyright and Similar
+     Rights.
+
+  e. Effective Technological Measures means those measures that, in the
+     absence of proper authority, may not be circumvented under laws
+     fulfilling obligations under Article 11 of the WIPO Copyright
+     Treaty adopted on December 20, 1996, and/or similar international
+     agreements.
+
+  f. Exceptions and Limitations means fair use, fair dealing, and/or
+     any other exception or limitation to Copyright and Similar Rights
+     that applies to Your use of the Licensed Material.
+
+  g. License Elements means the license attributes listed in the name
+     of a Creative Commons Public License. The License Elements of this
+     Public License are Attribution and ShareAlike.
+
+  h. Licensed Material means the artistic or literary work, database,
+     or other material to which the Licensor applied this Public
+     License.
+
+  i. Licensed Rights means the rights granted to You subject to the
+     terms and conditions of this Public License, which are limited to
+     all Copyright and Similar Rights that apply to Your use of the
+     Licensed Material and that the Licensor has authority to license.
+
+  j. Licensor means the individual(s) or entity(ies) granting rights
+     under this Public License.
+
+  k. Share means to provide material to the public by any means or
+     process that requires permission under the Licensed Rights, such
+     as reproduction, public display, public performance, distribution,
+     dissemination, communication, or importation, and to make material
+     available to the public including in ways that members of the
+     public may access the material from a place and at a time
+     individually chosen by them.
+
+  l. Sui Generis Database Rights means rights other than copyright
+     resulting from Directive 96/9/EC of the European Parliament and of
+     the Council of 11 March 1996 on the legal protection of databases,
+     as amended and/or succeeded, as well as other essentially
+     equivalent rights anywhere in the world.
+
+  m. You means the individual or entity exercising the Licensed Rights
+     under this Public License. Your has a corresponding meaning.
+
+
+Section 2 -- Scope.
+
+  a. License grant.
+
+       1. Subject to the terms and conditions of this Public License,
+          the Licensor hereby grants You a worldwide, royalty-free,
+          non-sublicensable, non-exclusive, irrevocable license to
+          exercise the Licensed Rights in the Licensed Material to:
+
+            a. reproduce and Share the Licensed Material, in whole or
+               in part; and
+
+            b. produce, reproduce, and Share Adapted Material.
+
+       2. Exceptions and Limitations. For the avoidance of doubt, where
+          Exceptions and Limitations apply to Your use, this Public
+          License does not apply, and You do not need to comply with
+          its terms and conditions.
+
+       3. Term. The term of this Public License is specified in Section
+          6(a).
+
+       4. Media and formats; technical modifications allowed. The
+          Licensor authorizes You to exercise the Licensed Rights in
+          all media and formats whether now known or hereafter created,
+          and to make technical modifications necessary to do so. The
+          Licensor waives and/or agrees not to assert any right or
+          authority to forbid You from making technical modifications
+          necessary to exercise the Licensed Rights, including
+          technical modifications necessary to circumvent Effective
+          Technological Measures. For purposes of this Public License,
+          simply making modifications authorized by this Section 2(a)
+          (4) never produces Adapted Material.
+
+       5. Downstream recipients.
+
+            a. Offer from the Licensor -- Licensed Material. Every
+               recipient of the Licensed Material automatically
+               receives an offer from the Licensor to exercise the
+               Licensed Rights under the terms and conditions of this
+               Public License.
+
+            b. Additional offer from the Licensor -- Adapted Material.
+               Every recipient of Adapted Material from You
+               automatically receives an offer from the Licensor to
+               exercise the Licensed Rights in the Adapted Material
+               under the conditions of the Adapter's License You apply.
+
+            c. No downstream restrictions. You may not offer or impose
+               any additional or different terms or conditions on, or
+               apply any Effective Technological Measures to, the
+               Licensed Material if doing so restricts exercise of the
+               Licensed Rights by any recipient of the Licensed
+               Material.
+
+       6. No endorsement. Nothing in this Public License constitutes or
+          may be construed as permission to assert or imply that You
+          are, or that Your use of the Licensed Material is, connected
+          with, or sponsored, endorsed, or granted official status by,
+          the Licensor or others designated to receive attribution as
+          provided in Section 3(a)(1)(A)(i).
+
+  b. Other rights.
+
+       1. Moral rights, such as the right of integrity, are not
+          licensed under this Public License, nor are publicity,
+          privacy, and/or other similar personality rights; however, to
+          the extent possible, the Licensor waives and/or agrees not to
+          assert any such rights held by the Licensor to the limited
+          extent necessary to allow You to exercise the Licensed
+          Rights, but not otherwise.
+
+       2. Patent and trademark rights are not licensed under this
+          Public License.
+
+       3. To the extent possible, the Licensor waives any right to
+          collect royalties from You for the exercise of the Licensed
+          Rights, whether directly or through a collecting society
+          under any voluntary or waivable statutory or compulsory
+          licensing scheme. In all other cases the Licensor expressly
+          reserves any right to collect such royalties.
+
+
+Section 3 -- License Conditions.
+
+Your exercise of the Licensed Rights is expressly made subject to the
+following conditions.
+
+  a. Attribution.
+
+       1. If You Share the Licensed Material (including in modified
+          form), You must:
+
+            a. retain the following if it is supplied by the Licensor
+               with the Licensed Material:
+
+                 i. identification of the creator(s) of the Licensed
+                    Material and any others designated to receive
+                    attribution, in any reasonable manner requested by
+                    the Licensor (including by pseudonym if
+                    designated);
+
+                ii. a copyright notice;
+
+               iii. a notice that refers to this Public License;
+
+                iv. a notice that refers to the disclaimer of
+                    warranties;
+
+                 v. a URI or hyperlink to the Licensed Material to the
+                    extent reasonably practicable;
+
+            b. indicate if You modified the Licensed Material and
+               retain an indication of any previous modifications; and
+
+            c. indicate the Licensed Material is licensed under this
+               Public License, and include the text of, or the URI or
+               hyperlink to, this Public License.
+
+       2. You may satisfy the conditions in Section 3(a)(1) in any
+          reasonable manner based on the medium, means, and context in
+          which You Share the Licensed Material. For example, it may be
+          reasonable to satisfy the conditions by providing a URI or
+          hyperlink to a resource that includes the required
+          information.
+
+       3. If requested by the Licensor, You must remove any of the
+          information required by Section 3(a)(1)(A) to the extent
+          reasonably practicable.
+
+  b. ShareAlike.
+
+     In addition to the conditions in Section 3(a), if You Share
+     Adapted Material You produce, the following conditions also apply.
+
+       1. The Adapter's License You apply must be a Creative Commons
+          license with the same License Elements, this version or
+          later, or a BY-SA Compatible License.
+
+       2. You must include the text of, or the URI or hyperlink to, the
+          Adapter's License You apply. You may satisfy this condition
+          in any reasonable manner based on the medium, means, and
+          context in which You Share Adapted Material.
+
+       3. You may not offer or impose any additional or different terms
+          or conditions on, or apply any Effective Technological
+          Measures to, Adapted Material that restrict exercise of the
+          rights granted under the Adapter's License You apply.
+
+
+Section 4 -- Sui Generis Database Rights.
+
+Where the Licensed Rights include Sui Generis Database Rights that
+apply to Your use of the Licensed Material:
+
+  a. for the avoidance of doubt, Section 2(a)(1) grants You the right
+     to extract, reuse, reproduce, and Share all or a substantial
+     portion of the contents of the database;
+
+  b. if You include all or a substantial portion of the database
+     contents in a database in which You have Sui Generis Database
+     Rights, then the database in which You have Sui Generis Database
+     Rights (but not its individual contents) is Adapted Material,
+
+     including for purposes of Section 3(b); and
+  c. You must comply with the conditions in Section 3(a) if You Share
+     all or a substantial portion of the contents of the database.
+
+For the avoidance of doubt, this Section 4 supplements and does not
+replace Your obligations under this Public License where the Licensed
+Rights include other Copyright and Similar Rights.
+
+
+Section 5 -- Disclaimer of Warranties and Limitation of Liability.
+
+  a. UNLESS OTHERWISE SEPARATELY UNDERTAKEN BY THE LICENSOR, TO THE
+     EXTENT POSSIBLE, THE LICENSOR OFFERS THE LICENSED MATERIAL AS-IS
+     AND AS-AVAILABLE, AND MAKES NO REPRESENTATIONS OR WARRANTIES OF
+     ANY KIND CONCERNING THE LICENSED MATERIAL, WHETHER EXPRESS,
+     IMPLIED, STATUTORY, OR OTHER. THIS INCLUDES, WITHOUT LIMITATION,
+     WARRANTIES OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR
+     PURPOSE, NON-INFRINGEMENT, ABSENCE OF LATENT OR OTHER DEFECTS,
+     ACCURACY, OR THE PRESENCE OR ABSENCE OF ERRORS, WHETHER OR NOT
+     KNOWN OR DISCOVERABLE. WHERE DISCLAIMERS OF WARRANTIES ARE NOT
+     ALLOWED IN FULL OR IN PART, THIS DISCLAIMER MAY NOT APPLY TO YOU.
+
+  b. TO THE EXTENT POSSIBLE, IN NO EVENT WILL THE LICENSOR BE LIABLE
+     TO YOU ON ANY LEGAL THEORY (INCLUDING, WITHOUT LIMITATION,
+     NEGLIGENCE) OR OTHERWISE FOR ANY DIRECT, SPECIAL, INDIRECT,
+     INCIDENTAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY, OR OTHER LOSSES,
+     COSTS, EXPENSES, OR DAMAGES ARISING OUT OF THIS PUBLIC LICENSE OR
+     USE OF THE LICENSED MATERIAL, EVEN IF THE LICENSOR HAS BEEN
+     ADVISED OF THE POSSIBILITY OF SUCH LOSSES, COSTS, EXPENSES, OR
+     DAMAGES. WHERE A LIMITATION OF LIABILITY IS NOT ALLOWED IN FULL OR
+     IN PART, THIS LIMITATION MAY NOT APPLY TO YOU.
+
+  c. The disclaimer of warranties and limitation of liability provided
+     above shall be interpreted in a manner that, to the extent
+     possible, most closely approximates an absolute disclaimer and
+     waiver of all liability.
+
+
+Section 6 -- Term and Termination.
+
+  a. This Public License applies for the term of the Copyright and
+     Similar Rights licensed here. However, if You fail to comply with
+     this Public License, then Your rights under this Public License
+     terminate automatically.
+
+  b. Where Your right to use the Licensed Material has terminated under
+     Section 6(a), it reinstates:
+
+       1. automatically as of the date the violation is cured, provided
+          it is cured within 30 days of Your discovery of the
+          violation; or
+
+       2. upon express reinstatement by the Licensor.
+
+     For the avoidance of doubt, this Section 6(b) does not affect any
+     right the Licensor may have to seek remedies for Your violations
+     of this Public License.
+
+  c. For the avoidance of doubt, the Licensor may also offer the
+     Licensed Material under separate terms or conditions or stop
+     distributing the Licensed Material at any time; however, doing so
+     will not terminate this Public License.
+
+  d. Sections 1, 5, 6, 7, and 8 survive termination of this Public
+     License.
+
+
+Section 7 -- Other Terms and Conditions.
+
+  a. The Licensor shall not be bound by any additional or different
+     terms or conditions communicated by You unless expressly agreed.
+
+  b. Any arrangements, understandings, or agreements regarding the
+     Licensed Material not stated herein are separate from and
+     independent of the terms and conditions of this Public License.
+
+
+Section 8 -- Interpretation.
+
+  a. For the avoidance of doubt, this Public License does not, and
+     shall not be interpreted to, reduce, limit, restrict, or impose
+     conditions on any use of the Licensed Material that could lawfully
+     be made without permission under this Public License.
+
+  b. To the extent possible, if any provision of this Public License is
+     deemed unenforceable, it shall be automatically reformed to the
+     minimum extent necessary to make it enforceable. If the provision
+     cannot be reformed, it shall be severed from this Public License
+     without affecting the enforceability of the remaining terms and
+     conditions.
+
+  c. No term or condition of this Public License will be waived and no
+     failure to comply consented to unless expressly agreed to by the
+     Licensor.
+
+  d. Nothing in this Public License constitutes or may be interpreted
+     as a limitation upon, or waiver of, any privileges and immunities
+     that apply to the Licensor or You, including from the legal
+     processes of any jurisdiction or authority.
+
+
+=======================================================================
+
+Creative Commons is not a party to its public
+licenses. Notwithstanding, Creative Commons may elect to apply one of
+its public licenses to material it publishes and in those instances
+will be considered the “Licensor.” The text of the Creative Commons
+public licenses is dedicated to the public domain under the CC0 Public
+Domain Dedication. Except for the limited purpose of indicating that
+material is shared under a Creative Commons public license or as
+otherwise permitted by the Creative Commons policies published at
+creativecommons.org/policies, Creative Commons does not authorize the
+use of the trademark "Creative Commons" or any other trademark or logo
+of Creative Commons without its prior written consent including,
+without limitation, in connection with any unauthorized modifications
+to any of its public licenses or any other arrangements,
+understandings, or agreements concerning use of licensed material. For
+the avoidance of doubt, this paragraph does not form part of the
+public licenses.
+
+Creative Commons may be contacted at creativecommons.org.
diff --git a/LICENSES/CC0-1.0.txt b/LICENSES/CC0-1.0.txt
new file mode 100644 (file)
index 0000000..0e259d4
--- /dev/null
@@ -0,0 +1,121 @@
+Creative Commons Legal Code
+
+CC0 1.0 Universal
+
+    CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE
+    LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN
+    ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS
+    INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES
+    REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS
+    PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM
+    THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED
+    HEREUNDER.
+
+Statement of Purpose
+
+The laws of most jurisdictions throughout the world automatically confer
+exclusive Copyright and Related Rights (defined below) upon the creator
+and subsequent owner(s) (each and all, an "owner") of an original work of
+authorship and/or a database (each, a "Work").
+
+Certain owners wish to permanently relinquish those rights to a Work for
+the purpose of contributing to a commons of creative, cultural and
+scientific works ("Commons") that the public can reliably and without fear
+of later claims of infringement build upon, modify, incorporate in other
+works, reuse and redistribute as freely as possible in any form whatsoever
+and for any purposes, including without limitation commercial purposes.
+These owners may contribute to the Commons to promote the ideal of a free
+culture and the further production of creative, cultural and scientific
+works, or to gain reputation or greater distribution for their Work in
+part through the use and efforts of others.
+
+For these and/or other purposes and motivations, and without any
+expectation of additional consideration or compensation, the person
+associating CC0 with a Work (the "Affirmer"), to the extent that he or she
+is an owner of Copyright and Related Rights in the Work, voluntarily
+elects to apply CC0 to the Work and publicly distribute the Work under its
+terms, with knowledge of his or her Copyright and Related Rights in the
+Work and the meaning and intended legal effect of CC0 on those rights.
+
+1. Copyright and Related Rights. A Work made available under CC0 may be
+protected by copyright and related or neighboring rights ("Copyright and
+Related Rights"). Copyright and Related Rights include, but are not
+limited to, the following:
+
+  i. the right to reproduce, adapt, distribute, perform, display,
+     communicate, and translate a Work;
+ ii. moral rights retained by the original author(s) and/or performer(s);
+iii. publicity and privacy rights pertaining to a person's image or
+     likeness depicted in a Work;
+ iv. rights protecting against unfair competition in regards to a Work,
+     subject to the limitations in paragraph 4(a), below;
+  v. rights protecting the extraction, dissemination, use and reuse of data
+     in a Work;
+ vi. database rights (such as those arising under Directive 96/9/EC of the
+     European Parliament and of the Council of 11 March 1996 on the legal
+     protection of databases, and under any national implementation
+     thereof, including any amended or successor version of such
+     directive); and
+vii. other similar, equivalent or corresponding rights throughout the
+     world based on applicable law or treaty, and any national
+     implementations thereof.
+
+2. Waiver. To the greatest extent permitted by, but not in contravention
+of, applicable law, Affirmer hereby overtly, fully, permanently,
+irrevocably and unconditionally waives, abandons, and surrenders all of
+Affirmer's Copyright and Related Rights and associated claims and causes
+of action, whether now known or unknown (including existing as well as
+future claims and causes of action), in the Work (i) in all territories
+worldwide, (ii) for the maximum duration provided by applicable law or
+treaty (including future time extensions), (iii) in any current or future
+medium and for any number of copies, and (iv) for any purpose whatsoever,
+including without limitation commercial, advertising or promotional
+purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each
+member of the public at large and to the detriment of Affirmer's heirs and
+successors, fully intending that such Waiver shall not be subject to
+revocation, rescission, cancellation, termination, or any other legal or
+equitable action to disrupt the quiet enjoyment of the Work by the public
+as contemplated by Affirmer's express Statement of Purpose.
+
+3. Public License Fallback. Should any part of the Waiver for any reason
+be judged legally invalid or ineffective under applicable law, then the
+Waiver shall be preserved to the maximum extent permitted taking into
+account Affirmer's express Statement of Purpose. In addition, to the
+extent the Waiver is so judged Affirmer hereby grants to each affected
+person a royalty-free, non transferable, non sublicensable, non exclusive,
+irrevocable and unconditional license to exercise Affirmer's Copyright and
+Related Rights in the Work (i) in all territories worldwide, (ii) for the
+maximum duration provided by applicable law or treaty (including future
+time extensions), (iii) in any current or future medium and for any number
+of copies, and (iv) for any purpose whatsoever, including without
+limitation commercial, advertising or promotional purposes (the
+"License"). The License shall be deemed effective as of the date CC0 was
+applied by Affirmer to the Work. Should any part of the License for any
+reason be judged legally invalid or ineffective under applicable law, such
+partial invalidity or ineffectiveness shall not invalidate the remainder
+of the License, and in such case Affirmer hereby affirms that he or she
+will not (i) exercise any of his or her remaining Copyright and Related
+Rights in the Work or (ii) assert any associated claims and causes of
+action with respect to the Work, in either case contrary to Affirmer's
+express Statement of Purpose.
+
+4. Limitations and Disclaimers.
+
+ a. No trademark or patent rights held by Affirmer are waived, abandoned,
+    surrendered, licensed or otherwise affected by this document.
+ b. Affirmer offers the Work as-is and makes no representations or
+    warranties of any kind concerning the Work, express, implied,
+    statutory or otherwise, including without limitation warranties of
+    title, merchantability, fitness for a particular purpose, non
+    infringement, or the absence of latent or other defects, accuracy, or
+    the present or absence of errors, whether or not discoverable, all to
+    the greatest extent permissible under applicable law.
+ c. Affirmer disclaims responsibility for clearing rights of other persons
+    that may apply to the Work or any use thereof, including without
+    limitation any person's Copyright and Related Rights in the Work.
+    Further, Affirmer disclaims responsibility for obtaining any necessary
+    consents, permissions or other rights required for any use of the
+    Work.
+ d. Affirmer understands and acknowledges that Creative Commons is not a
+    party to this document and has no duty or obligation with respect to
+    this CC0 or use of the Work.
diff --git a/LICENSES/GPL-2.0-only.txt b/LICENSES/GPL-2.0-only.txt
new file mode 100644 (file)
index 0000000..d159169
--- /dev/null
@@ -0,0 +1,339 @@
+                    GNU GENERAL PUBLIC LICENSE
+                       Version 2, June 1991
+
+ Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+                            Preamble
+
+  The licenses for most software are designed to take away your
+freedom to share and change it.  By contrast, the GNU General Public
+License is intended to guarantee your freedom to share and change free
+software--to make sure the software is free for all its users.  This
+General Public License applies to most of the Free Software
+Foundation's software and to any other program whose authors commit to
+using it.  (Some other Free Software Foundation software is covered by
+the GNU Lesser General Public License instead.)  You can apply it to
+your programs, too.
+
+  When we speak of free software, we are referring to freedom, not
+price.  Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+this service if you wish), that you receive source code or can get it
+if you want it, that you can change the software or use pieces of it
+in new free programs; and that you know you can do these things.
+
+  To protect your rights, we need to make restrictions that forbid
+anyone to deny you these rights or to ask you to surrender the rights.
+These restrictions translate to certain responsibilities for you if you
+distribute copies of the software, or if you modify it.
+
+  For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must give the recipients all the rights that
+you have.  You must make sure that they, too, receive or can get the
+source code.  And you must show them these terms so they know their
+rights.
+
+  We protect your rights with two steps: (1) copyright the software, and
+(2) offer you this license which gives you legal permission to copy,
+distribute and/or modify the software.
+
+  Also, for each author's protection and ours, we want to make certain
+that everyone understands that there is no warranty for this free
+software.  If the software is modified by someone else and passed on, we
+want its recipients to know that what they have is not the original, so
+that any problems introduced by others will not reflect on the original
+authors' reputations.
+
+  Finally, any free program is threatened constantly by software
+patents.  We wish to avoid the danger that redistributors of a free
+program will individually obtain patent licenses, in effect making the
+program proprietary.  To prevent this, we have made it clear that any
+patent must be licensed for everyone's free use or not licensed at all.
+
+  The precise terms and conditions for copying, distribution and
+modification follow.
+
+                    GNU GENERAL PUBLIC LICENSE
+   TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+  0. This License applies to any program or other work which contains
+a notice placed by the copyright holder saying it may be distributed
+under the terms of this General Public License.  The "Program", below,
+refers to any such program or work, and a "work based on the Program"
+means either the Program or any derivative work under copyright law:
+that is to say, a work containing the Program or a portion of it,
+either verbatim or with modifications and/or translated into another
+language.  (Hereinafter, translation is included without limitation in
+the term "modification".)  Each licensee is addressed as "you".
+
+Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope.  The act of
+running the Program is not restricted, and the output from the Program
+is covered only if its contents constitute a work based on the
+Program (independent of having been made by running the Program).
+Whether that is true depends on what the Program does.
+
+  1. You may copy and distribute verbatim copies of the Program's
+source code as you receive it, in any medium, provided that you
+conspicuously and appropriately publish on each copy an appropriate
+copyright notice and disclaimer of warranty; keep intact all the
+notices that refer to this License and to the absence of any warranty;
+and give any other recipients of the Program a copy of this License
+along with the Program.
+
+You may charge a fee for the physical act of transferring a copy, and
+you may at your option offer warranty protection in exchange for a fee.
+
+  2. You may modify your copy or copies of the Program or any portion
+of it, thus forming a work based on the Program, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+    a) You must cause the modified files to carry prominent notices
+    stating that you changed the files and the date of any change.
+
+    b) You must cause any work that you distribute or publish, that in
+    whole or in part contains or is derived from the Program or any
+    part thereof, to be licensed as a whole at no charge to all third
+    parties under the terms of this License.
+
+    c) If the modified program normally reads commands interactively
+    when run, you must cause it, when started running for such
+    interactive use in the most ordinary way, to print or display an
+    announcement including an appropriate copyright notice and a
+    notice that there is no warranty (or else, saying that you provide
+    a warranty) and that users may redistribute the program under
+    these conditions, and telling the user how to view a copy of this
+    License.  (Exception: if the Program itself is interactive but
+    does not normally print such an announcement, your work based on
+    the Program is not required to print an announcement.)
+
+These requirements apply to the modified work as a whole.  If
+identifiable sections of that work are not derived from the Program,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works.  But when you
+distribute the same sections as part of a whole which is a work based
+on the Program, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Program.
+
+In addition, mere aggregation of another work not based on the Program
+with the Program (or with a work based on the Program) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+  3. You may copy and distribute the Program (or a work based on it,
+under Section 2) in object code or executable form under the terms of
+Sections 1 and 2 above provided that you also do one of the following:
+
+    a) Accompany it with the complete corresponding machine-readable
+    source code, which must be distributed under the terms of Sections
+    1 and 2 above on a medium customarily used for software interchange; or,
+
+    b) Accompany it with a written offer, valid for at least three
+    years, to give any third party, for a charge no more than your
+    cost of physically performing source distribution, a complete
+    machine-readable copy of the corresponding source code, to be
+    distributed under the terms of Sections 1 and 2 above on a medium
+    customarily used for software interchange; or,
+
+    c) Accompany it with the information you received as to the offer
+    to distribute corresponding source code.  (This alternative is
+    allowed only for noncommercial distribution and only if you
+    received the program in object code or executable form with such
+    an offer, in accord with Subsection b above.)
+
+The source code for a work means the preferred form of the work for
+making modifications to it.  For an executable work, complete source
+code means all the source code for all modules it contains, plus any
+associated interface definition files, plus the scripts used to
+control compilation and installation of the executable.  However, as a
+special exception, the source code distributed need not include
+anything that is normally distributed (in either source or binary
+form) with the major components (compiler, kernel, and so on) of the
+operating system on which the executable runs, unless that component
+itself accompanies the executable.
+
+If distribution of executable or object code is made by offering
+access to copy from a designated place, then offering equivalent
+access to copy the source code from the same place counts as
+distribution of the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+  4. You may not copy, modify, sublicense, or distribute the Program
+except as expressly provided under this License.  Any attempt
+otherwise to copy, modify, sublicense or distribute the Program is
+void, and will automatically terminate your rights under this License.
+However, parties who have received copies, or rights, from you under
+this License will not have their licenses terminated so long as such
+parties remain in full compliance.
+
+  5. You are not required to accept this License, since you have not
+signed it.  However, nothing else grants you permission to modify or
+distribute the Program or its derivative works.  These actions are
+prohibited by law if you do not accept this License.  Therefore, by
+modifying or distributing the Program (or any work based on the
+Program), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Program or works based on it.
+
+  6. Each time you redistribute the Program (or any work based on the
+Program), the recipient automatically receives a license from the
+original licensor to copy, distribute or modify the Program subject to
+these terms and conditions.  You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties to
+this License.
+
+  7. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License.  If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Program at all.  For example, if a patent
+license would not permit royalty-free redistribution of the Program by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Program.
+
+If any portion of this section is held invalid or unenforceable under
+any particular circumstance, the balance of the section is intended to
+apply and the section as a whole is intended to apply in other
+circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system, which is
+implemented by public license practices.  Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+  8. If the distribution and/or use of the Program is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Program under this License
+may add an explicit geographical distribution limitation excluding
+those countries, so that distribution is permitted only in or among
+countries not thus excluded.  In such case, this License incorporates
+the limitation as if written in the body of this License.
+
+  9. The Free Software Foundation may publish revised and/or new versions
+of the General Public License from time to time.  Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+Each version is given a distinguishing version number.  If the Program
+specifies a version number of this License which applies to it and "any
+later version", you have the option of following the terms and conditions
+either of that version or of any later version published by the Free
+Software Foundation.  If the Program does not specify a version number of
+this License, you may choose any version ever published by the Free Software
+Foundation.
+
+  10. If you wish to incorporate parts of the Program into other free
+programs whose distribution conditions are different, write to the author
+to ask for permission.  For software which is copyrighted by the Free
+Software Foundation, write to the Free Software Foundation; we sometimes
+make exceptions for this.  Our decision will be guided by the two goals
+of preserving the free status of all derivatives of our free software and
+of promoting the sharing and reuse of software generally.
+
+                            NO WARRANTY
+
+  11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
+FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW.  EXCEPT WHEN
+OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
+PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
+OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.  THE ENTIRE RISK AS
+TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU.  SHOULD THE
+PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
+REPAIR OR CORRECTION.
+
+  12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
+REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
+INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
+OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
+TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
+YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
+PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGES.
+
+                     END OF TERMS AND CONDITIONS
+
+            How to Apply These Terms to Your New Programs
+
+  If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+  To do so, attach the following notices to the program.  It is safest
+to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+    <one line to give the program's name and a brief idea of what it does.>
+    Copyright (C) <year>  <name of author>
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License along
+    with this program; if not, write to the Free Software Foundation, Inc.,
+    51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+Also add information on how to contact you by electronic and paper mail.
+
+If the program is interactive, make it output a short notice like this
+when it starts in an interactive mode:
+
+    Gnomovision version 69, Copyright (C) year name of author
+    Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+    This is free software, and you are welcome to redistribute it
+    under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License.  Of course, the commands you use may
+be called something other than `show w' and `show c'; they could even be
+mouse-clicks or menu items--whatever suits your program.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the program, if
+necessary.  Here is a sample; alter the names:
+
+  Yoyodyne, Inc., hereby disclaims all copyright interest in the program
+  `Gnomovision' (which makes passes at compilers) written by James Hacker.
+
+  <signature of Ty Coon>, 1 April 1989
+  Ty Coon, President of Vice
+
+This General Public License does not permit incorporating your program into
+proprietary programs.  If your program is a subroutine library, you may
+consider it more useful to permit linking proprietary applications with the
+library.  If this is what you want to do, use the GNU Lesser General
+Public License instead of this License.
diff --git a/LICENSES/GPL-2.0-or-later.txt b/LICENSES/GPL-2.0-or-later.txt
new file mode 120000 (symlink)
index 0000000..0a87fbd
--- /dev/null
@@ -0,0 +1 @@
+GPL-2.0-only.txt
\ No newline at end of file
diff --git a/LICENSES/LGPL-2.1-or-later.txt b/LICENSES/LGPL-2.1-or-later.txt
new file mode 100644 (file)
index 0000000..e5ab03e
--- /dev/null
@@ -0,0 +1,502 @@
+                  GNU LESSER GENERAL PUBLIC LICENSE
+                       Version 2.1, February 1999
+
+ Copyright (C) 1991, 1999 Free Software Foundation, Inc.
+ 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+[This is the first released version of the Lesser GPL.  It also counts
+ as the successor of the GNU Library Public License, version 2, hence
+ the version number 2.1.]
+
+                            Preamble
+
+  The licenses for most software are designed to take away your
+freedom to share and change it.  By contrast, the GNU General Public
+Licenses are intended to guarantee your freedom to share and change
+free software--to make sure the software is free for all its users.
+
+  This license, the Lesser General Public License, applies to some
+specially designated software packages--typically libraries--of the
+Free Software Foundation and other authors who decide to use it.  You
+can use it too, but we suggest you first think carefully about whether
+this license or the ordinary General Public License is the better
+strategy to use in any particular case, based on the explanations below.
+
+  When we speak of free software, we are referring to freedom of use,
+not price.  Our General Public Licenses are designed to make sure that
+you have the freedom to distribute copies of free software (and charge
+for this service if you wish); that you receive source code or can get
+it if you want it; that you can change the software and use pieces of
+it in new free programs; and that you are informed that you can do
+these things.
+
+  To protect your rights, we need to make restrictions that forbid
+distributors to deny you these rights or to ask you to surrender these
+rights.  These restrictions translate to certain responsibilities for
+you if you distribute copies of the library or if you modify it.
+
+  For example, if you distribute copies of the library, whether gratis
+or for a fee, you must give the recipients all the rights that we gave
+you.  You must make sure that they, too, receive or can get the source
+code.  If you link other code with the library, you must provide
+complete object files to the recipients, so that they can relink them
+with the library after making changes to the library and recompiling
+it.  And you must show them these terms so they know their rights.
+
+  We protect your rights with a two-step method: (1) we copyright the
+library, and (2) we offer you this license, which gives you legal
+permission to copy, distribute and/or modify the library.
+
+  To protect each distributor, we want to make it very clear that
+there is no warranty for the free library.  Also, if the library is
+modified by someone else and passed on, the recipients should know
+that what they have is not the original version, so that the original
+author's reputation will not be affected by problems that might be
+introduced by others.
+
+  Finally, software patents pose a constant threat to the existence of
+any free program.  We wish to make sure that a company cannot
+effectively restrict the users of a free program by obtaining a
+restrictive license from a patent holder.  Therefore, we insist that
+any patent license obtained for a version of the library must be
+consistent with the full freedom of use specified in this license.
+
+  Most GNU software, including some libraries, is covered by the
+ordinary GNU General Public License.  This license, the GNU Lesser
+General Public License, applies to certain designated libraries, and
+is quite different from the ordinary General Public License.  We use
+this license for certain libraries in order to permit linking those
+libraries into non-free programs.
+
+  When a program is linked with a library, whether statically or using
+a shared library, the combination of the two is legally speaking a
+combined work, a derivative of the original library.  The ordinary
+General Public License therefore permits such linking only if the
+entire combination fits its criteria of freedom.  The Lesser General
+Public License permits more lax criteria for linking other code with
+the library.
+
+  We call this license the "Lesser" General Public License because it
+does Less to protect the user's freedom than the ordinary General
+Public License.  It also provides other free software developers Less
+of an advantage over competing non-free programs.  These disadvantages
+are the reason we use the ordinary General Public License for many
+libraries.  However, the Lesser license provides advantages in certain
+special circumstances.
+
+  For example, on rare occasions, there may be a special need to
+encourage the widest possible use of a certain library, so that it becomes
+a de-facto standard.  To achieve this, non-free programs must be
+allowed to use the library.  A more frequent case is that a free
+library does the same job as widely used non-free libraries.  In this
+case, there is little to gain by limiting the free library to free
+software only, so we use the Lesser General Public License.
+
+  In other cases, permission to use a particular library in non-free
+programs enables a greater number of people to use a large body of
+free software.  For example, permission to use the GNU C Library in
+non-free programs enables many more people to use the whole GNU
+operating system, as well as its variant, the GNU/Linux operating
+system.
+
+  Although the Lesser General Public License is Less protective of the
+users' freedom, it does ensure that the user of a program that is
+linked with the Library has the freedom and the wherewithal to run
+that program using a modified version of the Library.
+
+  The precise terms and conditions for copying, distribution and
+modification follow.  Pay close attention to the difference between a
+"work based on the library" and a "work that uses the library".  The
+former contains code derived from the library, whereas the latter must
+be combined with the library in order to run.
+
+                  GNU LESSER GENERAL PUBLIC LICENSE
+   TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+  0. This License Agreement applies to any software library or other
+program which contains a notice placed by the copyright holder or
+other authorized party saying it may be distributed under the terms of
+this Lesser General Public License (also called "this License").
+Each licensee is addressed as "you".
+
+  A "library" means a collection of software functions and/or data
+prepared so as to be conveniently linked with application programs
+(which use some of those functions and data) to form executables.
+
+  The "Library", below, refers to any such software library or work
+which has been distributed under these terms.  A "work based on the
+Library" means either the Library or any derivative work under
+copyright law: that is to say, a work containing the Library or a
+portion of it, either verbatim or with modifications and/or translated
+straightforwardly into another language.  (Hereinafter, translation is
+included without limitation in the term "modification".)
+
+  "Source code" for a work means the preferred form of the work for
+making modifications to it.  For a library, complete source code means
+all the source code for all modules it contains, plus any associated
+interface definition files, plus the scripts used to control compilation
+and installation of the library.
+
+  Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope.  The act of
+running a program using the Library is not restricted, and output from
+such a program is covered only if its contents constitute a work based
+on the Library (independent of the use of the Library in a tool for
+writing it).  Whether that is true depends on what the Library does
+and what the program that uses the Library does.
+
+  1. You may copy and distribute verbatim copies of the Library's
+complete source code as you receive it, in any medium, provided that
+you conspicuously and appropriately publish on each copy an
+appropriate copyright notice and disclaimer of warranty; keep intact
+all the notices that refer to this License and to the absence of any
+warranty; and distribute a copy of this License along with the
+Library.
+
+  You may charge a fee for the physical act of transferring a copy,
+and you may at your option offer warranty protection in exchange for a
+fee.
+
+  2. You may modify your copy or copies of the Library or any portion
+of it, thus forming a work based on the Library, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+    a) The modified work must itself be a software library.
+
+    b) You must cause the files modified to carry prominent notices
+    stating that you changed the files and the date of any change.
+
+    c) You must cause the whole of the work to be licensed at no
+    charge to all third parties under the terms of this License.
+
+    d) If a facility in the modified Library refers to a function or a
+    table of data to be supplied by an application program that uses
+    the facility, other than as an argument passed when the facility
+    is invoked, then you must make a good faith effort to ensure that,
+    in the event an application does not supply such function or
+    table, the facility still operates, and performs whatever part of
+    its purpose remains meaningful.
+
+    (For example, a function in a library to compute square roots has
+    a purpose that is entirely well-defined independent of the
+    application.  Therefore, Subsection 2d requires that any
+    application-supplied function or table used by this function must
+    be optional: if the application does not supply it, the square
+    root function must still compute square roots.)
+
+These requirements apply to the modified work as a whole.  If
+identifiable sections of that work are not derived from the Library,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works.  But when you
+distribute the same sections as part of a whole which is a work based
+on the Library, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote
+it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Library.
+
+In addition, mere aggregation of another work not based on the Library
+with the Library (or with a work based on the Library) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+  3. You may opt to apply the terms of the ordinary GNU General Public
+License instead of this License to a given copy of the Library.  To do
+this, you must alter all the notices that refer to this License, so
+that they refer to the ordinary GNU General Public License, version 2,
+instead of to this License.  (If a newer version than version 2 of the
+ordinary GNU General Public License has appeared, then you can specify
+that version instead if you wish.)  Do not make any other change in
+these notices.
+
+  Once this change is made in a given copy, it is irreversible for
+that copy, so the ordinary GNU General Public License applies to all
+subsequent copies and derivative works made from that copy.
+
+  This option is useful when you wish to copy part of the code of
+the Library into a program that is not a library.
+
+  4. You may copy and distribute the Library (or a portion or
+derivative of it, under Section 2) in object code or executable form
+under the terms of Sections 1 and 2 above provided that you accompany
+it with the complete corresponding machine-readable source code, which
+must be distributed under the terms of Sections 1 and 2 above on a
+medium customarily used for software interchange.
+
+  If distribution of object code is made by offering access to copy
+from a designated place, then offering equivalent access to copy the
+source code from the same place satisfies the requirement to
+distribute the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+  5. A program that contains no derivative of any portion of the
+Library, but is designed to work with the Library by being compiled or
+linked with it, is called a "work that uses the Library".  Such a
+work, in isolation, is not a derivative work of the Library, and
+therefore falls outside the scope of this License.
+
+  However, linking a "work that uses the Library" with the Library
+creates an executable that is a derivative of the Library (because it
+contains portions of the Library), rather than a "work that uses the
+library".  The executable is therefore covered by this License.
+Section 6 states terms for distribution of such executables.
+
+  When a "work that uses the Library" uses material from a header file
+that is part of the Library, the object code for the work may be a
+derivative work of the Library even though the source code is not.
+Whether this is true is especially significant if the work can be
+linked without the Library, or if the work is itself a library.  The
+threshold for this to be true is not precisely defined by law.
+
+  If such an object file uses only numerical parameters, data
+structure layouts and accessors, and small macros and small inline
+functions (ten lines or less in length), then the use of the object
+file is unrestricted, regardless of whether it is legally a derivative
+work.  (Executables containing this object code plus portions of the
+Library will still fall under Section 6.)
+
+  Otherwise, if the work is a derivative of the Library, you may
+distribute the object code for the work under the terms of Section 6.
+Any executables containing that work also fall under Section 6,
+whether or not they are linked directly with the Library itself.
+
+  6. As an exception to the Sections above, you may also combine or
+link a "work that uses the Library" with the Library to produce a
+work containing portions of the Library, and distribute that work
+under terms of your choice, provided that the terms permit
+modification of the work for the customer's own use and reverse
+engineering for debugging such modifications.
+
+  You must give prominent notice with each copy of the work that the
+Library is used in it and that the Library and its use are covered by
+this License.  You must supply a copy of this License.  If the work
+during execution displays copyright notices, you must include the
+copyright notice for the Library among them, as well as a reference
+directing the user to the copy of this License.  Also, you must do one
+of these things:
+
+    a) Accompany the work with the complete corresponding
+    machine-readable source code for the Library including whatever
+    changes were used in the work (which must be distributed under
+    Sections 1 and 2 above); and, if the work is an executable linked
+    with the Library, with the complete machine-readable "work that
+    uses the Library", as object code and/or source code, so that the
+    user can modify the Library and then relink to produce a modified
+    executable containing the modified Library.  (It is understood
+    that the user who changes the contents of definitions files in the
+    Library will not necessarily be able to recompile the application
+    to use the modified definitions.)
+
+    b) Use a suitable shared library mechanism for linking with the
+    Library.  A suitable mechanism is one that (1) uses at run time a
+    copy of the library already present on the user's computer system,
+    rather than copying library functions into the executable, and (2)
+    will operate properly with a modified version of the library, if
+    the user installs one, as long as the modified version is
+    interface-compatible with the version that the work was made with.
+
+    c) Accompany the work with a written offer, valid for at
+    least three years, to give the same user the materials
+    specified in Subsection 6a, above, for a charge no more
+    than the cost of performing this distribution.
+
+    d) If distribution of the work is made by offering access to copy
+    from a designated place, offer equivalent access to copy the above
+    specified materials from the same place.
+
+    e) Verify that the user has already received a copy of these
+    materials or that you have already sent this user a copy.
+
+  For an executable, the required form of the "work that uses the
+Library" must include any data and utility programs needed for
+reproducing the executable from it.  However, as a special exception,
+the materials to be distributed need not include anything that is
+normally distributed (in either source or binary form) with the major
+components (compiler, kernel, and so on) of the operating system on
+which the executable runs, unless that component itself accompanies
+the executable.
+
+  It may happen that this requirement contradicts the license
+restrictions of other proprietary libraries that do not normally
+accompany the operating system.  Such a contradiction means you cannot
+use both them and the Library together in an executable that you
+distribute.
+
+  7. You may place library facilities that are a work based on the
+Library side-by-side in a single library together with other library
+facilities not covered by this License, and distribute such a combined
+library, provided that the separate distribution of the work based on
+the Library and of the other library facilities is otherwise
+permitted, and provided that you do these two things:
+
+    a) Accompany the combined library with a copy of the same work
+    based on the Library, uncombined with any other library
+    facilities.  This must be distributed under the terms of the
+    Sections above.
+
+    b) Give prominent notice with the combined library of the fact
+    that part of it is a work based on the Library, and explaining
+    where to find the accompanying uncombined form of the same work.
+
+  8. You may not copy, modify, sublicense, link with, or distribute
+the Library except as expressly provided under this License.  Any
+attempt otherwise to copy, modify, sublicense, link with, or
+distribute the Library is void, and will automatically terminate your
+rights under this License.  However, parties who have received copies,
+or rights, from you under this License will not have their licenses
+terminated so long as such parties remain in full compliance.
+
+  9. You are not required to accept this License, since you have not
+signed it.  However, nothing else grants you permission to modify or
+distribute the Library or its derivative works.  These actions are
+prohibited by law if you do not accept this License.  Therefore, by
+modifying or distributing the Library (or any work based on the
+Library), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Library or works based on it.
+
+  10. Each time you redistribute the Library (or any work based on the
+Library), the recipient automatically receives a license from the
+original licensor to copy, distribute, link with or modify the Library
+subject to these terms and conditions.  You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties with
+this License.
+
+  11. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License.  If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Library at all.  For example, if a patent
+license would not permit royalty-free redistribution of the Library by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Library.
+
+If any portion of this section is held invalid or unenforceable under any
+particular circumstance, the balance of the section is intended to apply,
+and the section as a whole is intended to apply in other circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system which is
+implemented by public license practices.  Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+  12. If the distribution and/or use of the Library is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Library under this License may add
+an explicit geographical distribution limitation excluding those countries,
+so that distribution is permitted only in or among countries not thus
+excluded.  In such case, this License incorporates the limitation as if
+written in the body of this License.
+
+  13. The Free Software Foundation may publish revised and/or new
+versions of the Lesser General Public License from time to time.
+Such new versions will be similar in spirit to the present version,
+but may differ in detail to address new problems or concerns.
+
+Each version is given a distinguishing version number.  If the Library
+specifies a version number of this License which applies to it and
+"any later version", you have the option of following the terms and
+conditions either of that version or of any later version published by
+the Free Software Foundation.  If the Library does not specify a
+license version number, you may choose any version ever published by
+the Free Software Foundation.
+
+  14. If you wish to incorporate parts of the Library into other free
+programs whose distribution conditions are incompatible with these,
+write to the author to ask for permission.  For software which is
+copyrighted by the Free Software Foundation, write to the Free
+Software Foundation; we sometimes make exceptions for this.  Our
+decision will be guided by the two goals of preserving the free status
+of all derivatives of our free software and of promoting the sharing
+and reuse of software generally.
+
+                            NO WARRANTY
+
+  15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO
+WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW.
+EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR
+OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY
+KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE
+LIBRARY IS WITH YOU.  SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME
+THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+  16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN
+WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY
+AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU
+FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR
+CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE
+LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING
+RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A
+FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF
+SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
+DAMAGES.
+
+                     END OF TERMS AND CONDITIONS
+
+           How to Apply These Terms to Your New Libraries
+
+  If you develop a new library, and you want it to be of the greatest
+possible use to the public, we recommend making it free software that
+everyone can redistribute and change.  You can do so by permitting
+redistribution under these terms (or, alternatively, under the terms of the
+ordinary General Public License).
+
+  To apply these terms, attach the following notices to the library.  It is
+safest to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least the
+"copyright" line and a pointer to where the full notice is found.
+
+    <one line to give the library's name and a brief idea of what it does.>
+    Copyright (C) <year>  <name of author>
+
+    This library is free software; you can redistribute it and/or
+    modify it under the terms of the GNU Lesser General Public
+    License as published by the Free Software Foundation; either
+    version 2.1 of the License, or (at your option) any later version.
+
+    This library is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+    Lesser General Public License for more details.
+
+    You should have received a copy of the GNU Lesser General Public
+    License along with this library; if not, write to the Free Software
+    Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+
+Also add information on how to contact you by electronic and paper mail.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the library, if
+necessary.  Here is a sample; alter the names:
+
+  Yoyodyne, Inc., hereby disclaims all copyright interest in the
+  library `Frob' (a library for tweaking knobs) written by James Random Hacker.
+
+  <signature of Ty Coon>, 1 April 1990
+  Ty Coon, President of Vice
+
+That's all there is to it!
diff --git a/LICENSES/Linux-syscall-note.txt b/LICENSES/Linux-syscall-note.txt
new file mode 100644 (file)
index 0000000..9abdad7
--- /dev/null
@@ -0,0 +1,25 @@
+SPDX-Exception-Identifier: Linux-syscall-note
+SPDX-URL: https://spdx.org/licenses/Linux-syscall-note.html
+SPDX-Licenses: GPL-2.0, GPL-2.0+, GPL-1.0+, LGPL-2.0, LGPL-2.0+, LGPL-2.1, LGPL-2.1+, GPL-2.0-only, GPL-2.0-or-later
+Usage-Guide:
+  This exception is used together with one of the above SPDX-Licenses
+  to mark user space API (uapi) header files so they can be included
+  into non GPL compliant user space application code.
+  To use this exception add it with the keyword WITH to one of the
+  identifiers in the SPDX-Licenses tag:
+    SPDX-License-Identifier: <SPDX-License> WITH Linux-syscall-note
+License-Text:
+
+   NOTE! This copyright does *not* cover user programs that use kernel
+ services by normal system calls - this is merely considered normal use
+ of the kernel, and does *not* fall under the heading of "derived work".
+ Also note that the GPL below is copyrighted by the Free Software
+ Foundation, but the instance of code that it refers to (the Linux
+ kernel) is copyrighted by me and others who actually wrote it.
+
+ Also note that the only valid version of the GPL as far as the kernel
+ is concerned is _this_ particular version of the license (ie v2, not
+ v2.2 or v3.x or whatever), unless explicitly otherwise stated.
+
+                       Linus Torvalds
+
diff --git a/Makefile.am b/Makefile.am
new file mode 100644 (file)
index 0000000..2ace901
--- /dev/null
@@ -0,0 +1,51 @@
+# SPDX-License-Identifier: GPL-2.0-or-later
+# SPDX-FileCopyrightText: 2017-2021 Bartosz Golaszewski <bartekgola@gmail.com>
+
+ACLOCAL_AMFLAGS = -I m4
+AUTOMAKE_OPTIONS = foreign
+SUBDIRS = include lib contrib
+
+EXTRA_DIST = \
+       LICENSES/GPL-2.0-or-later.txt \
+       LICENSES/Apache-2.0.txt \
+       LICENSES/LGPL-2.1-or-later.txt \
+       LICENSES/CC-BY-SA-4.0.txt \
+       LICENSES/CC0-1.0.txt \
+       LICENSES/GPL-2.0-only.txt \
+       LICENSES/Linux-syscall-note.txt \
+       LICENSES/BSD-3-Clause.txt
+
+if WITH_EXAMPLES
+
+SUBDIRS += examples
+
+endif
+
+if WITH_TOOLS
+
+SUBDIRS += tools man
+
+endif
+
+if WITH_TESTS
+
+SUBDIRS += tests
+
+endif
+
+# Build bindings after core tests. When building tests for bindings, we need
+# libgpiosim to be already present.
+SUBDIRS += bindings
+
+if HAS_DOXYGEN
+
+doc: Doxyfile
+       $(AM_V_GEN)doxygen Doxyfile
+.PHONY: doc
+
+clean-local:
+       rm -rf doc
+
+EXTRA_DIST += Doxyfile
+
+endif
diff --git a/NEWS b/NEWS
new file mode 100644 (file)
index 0000000..49da4dc
--- /dev/null
+++ b/NEWS
@@ -0,0 +1,398 @@
+# SPDX-License-Identifier: CC-BY-SA-4.0
+# SPDX-FileCopyrightText: 2017-2021 Bartosz Golaszewski <bartekgola@gmail.com>
+# SPDX-FileCopyrightText: 2023 Bartosz Golaszewski <bartosz.golaszewski@linaro.org>
+
+libgpiod v2.1.3
+===============
+
+Bug fixes:
+- fix C++ tests with recent kernels which introduced stricter reconfigure
+  behavior
+- fix a use-after-free bug in python bindings
+- fix passing the event clock property to line requests in python bindings
+- fix a memory leak in tools
+- make sure the string buffers in line-info and chip-info are big enough to not
+  truncate the strings they hold below the size accepted by the kernel
+
+libgpiod v2.1.2
+===============
+
+Bug fixes:
+- fix C++ bindings build using slibtool
+- accept the new style automatic GPIO chip labels from gpio-sim in bash tests
+
+Misc:
+- relicense C++ bindings under LGPL-2.1-or-later
+
+libgpiod v2.1.1
+===============
+
+Bug fixes:
+- remove buggy and unnecessary flags sanitization from line-config
+- fix python bindings installation with Makefile build
+- sanitize the return values of GPIO ioctl()s which in some cases may be
+  erroneously positive
+
+libgpiod v2.1
+=============
+
+New features:
+- port tests to shunit2
+- add a sample Android build file
+- add code examples to the core library and replace existing reimplementations
+  of gpio-tools with dedicated examples in bindings
+- don't install test executables
+- add idle-timeout option to gpiomon and gpionotify
+- provide gpiod_line_request_get_chip_name() and respective wrappers in
+  bindings
+
+Improvements:
+- add more tests for various corner-cases and improve coverage
+- documentation improvements
+- drop dependencies that make it impossible to build libgpiod with bionic libc
+- remove dead code
+- use AM_V_GEN where applicable in build
+
+Bug fixes:
+- fix a segfault in the GLib wrapper around libgpiosim
+- fix a race condition in libgpiosim
+- remove an implicit dependency on ncurses in gpio-tools tests
+- make value toggling in gpio-tools tests more reliable by removing hard-coded
+  sleeps in favor of actively waiting for value changes
+- sanitize the arguments in gpiod_line_config_set_output_values()
+- make the chip file descriptor blocking in the core library so that calls to
+  gpiod_chip_read_info_event() behave as advertised in the docs
+- fix setting the event clock type in gpiomon
+- fix the regex pattern for version strings
+- fix some test cases
+- drop profiling flags from tests' Makefile
+- don't use the same chip from different threads
+
+libgpiod v2.0
+=============
+
+This is a major release that breaks compatiblity with the v1.6.x series. The
+entire data model has been overhauled in order to make using the library more
+intuitive and less cumbersome, while also making the code future-proof and
+extensible. Please refer to the documentation for details.
+
+New features:
+- rework the entire API: core C library as well as C++ and Python bindings
+- rework the command-line tools in order to make them more line-name-oriented
+- drop gpiofind as tools can now resolve line names on their own
+- add the interactive mode to gpioset
+- add Rust bindings
+- make tests work with the gpio-sim kernel module
+
+libgpiod v1.6
+=============
+
+New features:
+- add a standardized '__version__' module attribute in Python bindings
+- print the bias flags info (if set) in gpioinfo
+
+Improvements:
+- remove unnecessary indirection in free_dirs() in iterator code
+- put all ABI versions next to one another in configure.ac
+- improve std namespace resolution in C++ bindings
+- add more checks for non-standard functions in configure.ac
+- various code size improvements
+- enforce gnu89 C standard in makefiles
+- many documentation improvements
+- unduplicate signalfd() handling in tools
+- fix a forward declaration for line_event in C++ bindings
+
+Bug fixes:
+- relax is_gpiochip_cdev() for symbolic links
+- make gpiod_line_get_value_bulk() work for bulks of lines requested for
+  events, not only those requested for values
+- fix regex patterns for timestamps in gpiomon test cases
+- remove leftover asserts from tests
+- fix unit conversion in event timestamp calculation in C++ bindings
+- fix reading subset of available events in core library
+
+libgpiod v1.5
+=============
+
+New features:
+- switched to using the GLib testing framework for core library tests and BATS
+  (Bash Automated Testing System) for command-line tools
+- used Catch2 C++ testing framework to implement a proper test-suite for C++
+  bindings while also reusing the API provided by libgpiomockup
+- used Python's unittest package to implement a proper test suite for Python
+  bindings and reused libgpiockup again
+- provided line::update() and Line.update() routines for C++ and Python
+  bindings respectively allowing to update the line info from bindings as well
+- added support for bias flags which are a new functionality first available in
+  linux v5.5; subsequently the library now requires v5.5 kernel headers to
+  build; the new flags are supported in the core library, C++ and Python
+  bindings as well as the command-line tools
+- added support for the new SET_CONFIG ioctl(): this too is a new functionality
+  added in linux v5.5; both features have been implemented in the library by
+  Kent Gibson
+- added routines for reading multiple line events at once to the core library,
+  C++ and Python bindings
+
+Improvements:
+- constified function arguments where applicable in libgpiomockup
+- fixed the name of the test exeucutable displayed at build time
+- improved the function pointer casting in Python bindings to avoid warnings
+  emitted by GCC8
+- switched to using the KERNEL_VERSION() macro in tests instead of handcoded
+  version parsing
+- improved the setup ordering in tests (setup libgpiomockup before checking
+  the kernel version
+- add 'extern "c"' to the libgpiomockup header to make it usable from C++
+- add chip index validation to libgpiomockup functions
+- check if the debugfs directory used by libgpiomockup is writable before
+  using it to set the pull of dummy lines
+- add several new test cases
+- improved Python example programs (made gpiomon's output similar to the
+  original tool, make gpioset wait for an ENTER pres by default)
+- fixed the major:minor number comparison between the device and sysfs
+- deprecated the gpiod_line_needs_update() function and removed the logic
+  behind it from the library
+- shrank the Python bindings a bit by directly returning the value from
+  PyErr_SetFromErrno()
+- dropped noexcept from methods which can throw in C++ bindings
+- switched to initializing the bitset with integers instead of strings in C++
+  bindings
+- allowed gpiod_line_set_value_bulk() to accept null pointers
+- when building Python bindings: check for the existence of python-config
+- improved the readability of help text messages for command-line tools
+- reworked the .gitignore file: added libtool scripts generated during
+  cross-compilation and split the main .gitignore into several fine-grained
+  files
+- fixed several misspellings
+- other minor tweaks and improvements
+
+Bug fixes:
+- fixed memory leaks in libgpiomockup
+- fixed memory leaks in the testing framework
+- fixed a segfault in error path in tests
+- make gpioinfo show lines claimed by the kernel as used even if they have no
+  named consumer
+- fixed the test cases validating the '--active-low' switch in gpiomon and
+  the GPIOHANDLE_REQUEST_ACTIVE_LOW flag in the core library after a fix
+  for incorrect behavior was merged in linux v5.2.7
+- stopped failing at init-time of libgpiomockup if gpio-mockup is already
+  loaded
+- added a missing throw keyword in error path in C++ bindings
+- fixed a segfault in Python bindings when calling Line.request() without
+  the consumer argument
+
+libgpiod v1.4
+=============
+
+New features:
+- updated the testing framework to work with linux v5.1 in which the debugfs
+  interface of the GPIO testing module changed in a backward incompatible way
+- factored out the code controlling the GPIO testing module into a separate
+  shared library that may be reused by future testing executables for different
+  language bindings
+- removed the --enable-install-tests build option and the make check target as
+  they were redundant, subsequently tests are now installed as a normal program
+  whenever they're enabled with --enable-tests
+
+Improvements:
+- removed unnecessary std::move calls from C++ bindings
+- added the explicit keyword to bool() operators in C++ bindings
+
+Bug fixes:
+- fix out of source build of man pages
+
+libgpiod v1.3
+=============
+
+New features:
+- the gpio-tools now have automatically generated (using help2man) man pages
+  that are bundled with the release tarball
+- support a singular 'default_val' argument in Line.request() in python
+  bindings
+- the test executable can now be installed to the bindir along with the
+  gpio-tools and the testing framework will look for the binaries in standard
+  locations if it's not run from the top source directory
+- gpiomon now supports line buffered output
+
+Improvements:
+- tweaks to the C API documentation
+- treewide unification of the naming of local variables
+- extended helptest in gpioset (explanation of the way the character device
+  works aimed at reducing user confusion when a GPIO line reverts to its
+  default value after gpioset exits)
+- the source directories have been rearranged and the src/ directory was
+  dropped, lib/ and tools/ now live in the top source directory
+- minor coding style fixes in python bindings, ctxless functions and tools
+- automatically generated documentation is now removed by 'make clean'
+- all Makefiles now use top_builddir instead of relative paths
+- code shrink in configure.ac
+- add a brief section about API documentation to README
+
+Bug fixes:
+- fix a segfault causing bug in C++ bindings
+- make bitset_cmp::operator() const as this is required by C++17
+- ignore 'remove' events from udev in the testing framework
+- don't segfault on num_lines = 0 in ctxless functions
+
+libgpiod v1.2
+=============
+
+New features:
+- new contextless event monitor that should replace the previous event loop
+  which caused problems on hardware that doesn't allow to watch both rising
+  and falling edge events
+- port gpiomon to the new event monitor
+- deprecate event loop routines
+
+Improvements:
+- many minor improvements and tweaks in the python module
+- new test cases for python bindings
+- add much more detailed documentation for python bindings
+- coding style improvements in gpio-tools
+- remove unicode characters from build scripts
+- improve the help text messages in gpio-tools
+- make gpiod_chip_open() and its variants verify that we're really trying to
+  open a character device associated with a GPIO chip
+
+Bug fixes:
+- fix memory leaks in python bindings
+- fix a memory corruption bug in python bindings
+- fix the default_vals argument in line request implementation in python
+  bindings
+- fix a compilation warning in python bindings
+- fix gpiod_Chip_find_lines() for nonexistent lines (python bindings)
+- add a missing include in C++ bindings examples
+- correctly display the version string in gpio-tools
+
+libgpiod v1.1
+=============
+
+New features:
+- add object-oriented C++ bindings
+- add object-oriented Python3 bindings
+- add several new helpers to the C API
+
+Improvements:
+- start using separate versioning schemes for API and ABI
+- use SPDX license identifiers and remove LGPL boilerplate
+- check for unexpanded macros in configure.ac
+
+Bug fixes:
+- include Doxyfile in the release tarball
+- fix the implicit-fallthrough warnings
+- make tests work together with gpio-mockup post v4.16 linux kernel
+- use reference counting for line file descriptors
+- correctly handle POLLNVAL when polling for events
+- fix the copyright notice in tools
+
+libgpiod v1.0
+=============
+
+NOTE: This is a major release - it breaks the API compatibility with
+      the 0.x.y series.
+
+New features:
+- remove custom error handling in favor of errnos
+- merge the two separate interfaces for event requests and regular line
+  requests
+- redesign the simple API
+- change the prefix of the high-level API from 'simple' to 'ctxless' (for
+  contextless) which better reflects its purpose
+- redesign the iterator API
+- make use of prefixes more consistent
+- rename symbols all over the place
+- various minor tweaks
+- add support for pkg-config
+
+Improvements:
+- add a bunch of helpers for line requests
+- split the library code into multiple source files by functionality
+- re-enable a test case previously broken by a bug in the kernel
+
+Bug fixes:
+- correctly handle signal interrupts when polling in gpiod_simple_event_loop()
+- fix the linking order when building with static libraries
+- pass the correct consumer string to gpiod_simple_get_value_multiple() in
+  gpioget
+- fix a line test case: don't use open-drain or open-source flags for input
+  mode
+- fix the flags passed to ar in order to supress a build warning
+- set the last error code in gpiod_chip_open_by_label() to ENOENT if a chip
+  can't be found
+- fix checking the kernel version in the test suite
+- fix various coding style issues
+- initialize the active low variable in gpiomon
+
+libgpiod v0.3
+=============
+
+New features:
+- gpiomon can now watch multiple lines at the same time and supports custom
+  output formats which can be specified using the --format argument
+- testing framework can now test external programs: test cases for gpio-tools
+  have been added
+
+Improvements:
+- improve error messages
+- improve README examples
+- configure script improvements
+
+Bug fixes:
+- use correct UAPI flags when requesting line events
+- capitalize 'GPIO' in error messages in gpioset, gpioget & gpiomon
+- tweak the error message on invalid arguments in gpiofind
+- don't ignore superfluous arguments and fix the displayed name for falling
+  edge events in gpiomon
+
+libgpiod v0.2
+=============
+
+New features:
+- relicensed under LGPLv2.1
+- implemented a unit testing framework together with a comprehensive
+  set of test cases
+- added a non-closing variant of the gpiochip iterator and foreach
+  macro [by Clemens Gruber]
+- added gpiod_chip_open_by_label()
+
+Improvements:
+- Makefiles & build commands have been reworked [by Thierry Reding]
+- documentation updates
+- code shrinkage here and there
+- coding style fixes
+- removed all designated initializers from the header for better standards
+  compliance
+
+Bug fixes:
+- fix the return value of gpiod_simple_event_loop()
+- don't try to process docs if doxygen is not installed
+- pass the O_CLOEXEC flag to open() when opening the GPIO chip device file
+- include <poll.h> instead of <sys/poll.h> in gpioset
+- fix a formatting issue in gpioinfo for chips with >100 GPIO lines
+- fix a bug when requesting both-edges event notifications
+- fix short options in gpiomon (short opt for --silent was missing)
+- correct the kernel headers requirements in README
+- include <time.h> for struct timespec
+- include <poll.h> instead of <sys/poll.h>
+- detect the version of strerror_r()
+
+libgpiod v0.1
+=============
+
+First version of libgpiod.
+
+It's currently possible to:
+- get and set the values of multiple GPIO lines with a single function call
+- monitor a GPIO line for events
+- enumerate all GPIO chips present in the system
+- enumerate all GPIO lines exposed by a chip
+- extract information about GPIO chips (label, name, number of lines)
+- extract information about GPIO lines (name, flags, state, user)
+
+Tools provided with the library are:
+- gpioget - read values from GPIO lines
+- gpioset - set values of GPIO lines
+- gpiodetect - list GPIO chips
+- gpioinfo - print info about GPIO lines exposed by a chip
+- gpiomon - monitor a GPIO line for events
+- gpiofind - find a GPIO line by name
diff --git a/README b/README
new file mode 100644 (file)
index 0000000..962dc06
--- /dev/null
+++ b/README
@@ -0,0 +1,302 @@
+# SPDX-License-Identifier: CC-BY-SA-4.0
+# SPDX-FileCopyrightText: 2017-2023 Bartosz Golaszewski <brgl@bgdev.pl>
+
+libgpiod
+========
+
+  libgpiod - C library and tools for interacting with the linux GPIO
+             character device (gpiod stands for GPIO device)
+
+Since linux 4.8 the GPIO sysfs interface is deprecated. User space should use
+the character device instead. This library encapsulates the ioctl calls and
+data structures behind a straightforward API.
+
+RATIONALE
+---------
+
+The new character device interface guarantees all allocated resources are
+freed after closing the device file descriptor and adds several new features
+that are not present in the obsolete sysfs interface (like event polling,
+setting/reading multiple values at once or open-source and open-drain GPIOs).
+
+Unfortunately interacting with the linux device file can no longer be done
+using only standard command-line tools. This is the reason for creating a
+library encapsulating the cumbersome, ioctl-based kernel-userspace interaction
+in a set of convenient functions and opaque data structures.
+
+Additionally this project contains a set of command-line tools that should
+allow an easy conversion of user scripts to using the character device.
+
+BUILDING
+--------
+
+This is a pretty standard autotools project. The core C library does not have
+any external dependencies other than the standard C library with GNU extensions.
+
+The command-line tools optionally depend on libedit for the interactive feature.
+
+To build the project (including command-line utilities) run:
+
+    ./autogen.sh --enable-tools=yes --prefix=<install path>
+    make
+    make install
+
+The autogen script will execute ./configure and pass all the command-line
+arguments to it.
+
+If building from release tarballs, the configure script is already provided and
+there's no need to invoke autogen.sh.
+
+For all configure features, see: ./configure --help.
+
+TOOLS
+-----
+
+There are currently six command-line tools available:
+
+* gpiodetect - list all gpiochips present on the system, their names, labels
+               and number of GPIO lines
+
+* gpioinfo   - list lines, their gpiochip, offset, name, and direction, and
+               if in use then the consumer name and any other configured
+               attributes, such as active state, bias, drive, edge detection
+               and debounce period
+
+* gpioget    - read values of specified GPIO lines
+
+* gpioset    - set values of specified GPIO lines, holding the lines until the
+               process is killed or otherwise exits
+
+* gpiomon    - wait for edge events on GPIO lines, specify which edges to watch
+               for, how many events to process before exiting, or if the events
+               should be reported to the console
+
+* gpionotify - wait for changed to the info for GPIO lines, specify which
+               changes to watch for, how many events to process before exiting,
+               or if the events should be reported to the console
+
+Examples:
+
+    (using a Raspberry Pi 4B)
+
+    # Detect the available gpiochips.
+    $ gpiodetect
+    gpiochip0 [pinctrl-bcm2711] (58 lines)
+    gpiochip1 [raspberrypi-exp-gpio] (8 lines)
+
+    # Read the info for all the lines on a gpiochip.
+    $ gpioinfo -c 1
+    gpiochip1 - 8 lines:
+     line   0: "BT_ON"          output
+     line   1: "WL_ON"          output
+     line   2: "PWR_LED_OFF"    output active-low consumer="led1"
+     line   3: "GLOBAL_RESET"   output
+     line   4: "VDD_SD_IO_SEL"  output consumer="vdd-sd-io"
+     line   5: "CAM_GPIO"       output consumer="cam1_regulator"
+     line   6: "SD_PWR_ON"      output consumer="sd_vcc_reg"
+     line   7: "SD_OC_N"        input
+
+    # Read the info for particular lines.
+    $ ./gpioinfo PWR_LED_OFF STATUS_LED_G_CLK GLOBAL_RESET
+    gpiochip0 42 "STATUS_LED_G_CLK" output consumer="led0"
+    gpiochip1 2 "PWR_LED_OFF"    output active-low consumer="led1"
+    gpiochip1 3 "GLOBAL_RESET"   output
+
+    # Read the value of a single GPIO line by name.
+    $ gpioget RXD1
+    "RXD1"=active
+
+    # Read the value of a single GPIO line by chip and offset.
+    $ gpioget -c 0 15
+    "15"=active
+
+    # Read the value of a single GPIO line as a numeric value.
+    $ gpioget --numeric RXD1
+    1
+
+    # Read two values at the same time. Set the active state of the lines
+    # to low and without quoted names.
+    $ gpioget --active-low --unquoted GPIO23 GPIO24
+    GPIO23=active GPIO24=active
+
+    # Set the value of a line and hold the line until killed.
+    $ gpioset GPIO23=1
+
+    # Set values of two lines, then daemonize and hold the lines.
+    $ gpioset --daemonize GPIO23=1 GPIO24=0
+
+    # Set the value of a single line, hold it for 20ms, then exit.
+    $ gpioset --hold-period 20ms -t0 GPIO23=1
+
+    # Blink an LED on GPIO22 at 1Hz
+    $ gpioset -t500ms GPIO22=1
+
+    # Blink an LED on GPIO22 at 1Hz with a 20% duty cycle
+    $ gpioset -t200ms,800ms GPIO22=1
+
+    # Set some lines interactively (requires --enable-gpioset-interative)
+    $ gpioset --interactive --unquoted GPIO23=inactive GPIO24=active
+    gpioset> get
+    GPIO23=inactive GPIO24=active
+    gpioset> toggle
+    gpioset> get
+    GPIO23=active GPIO24=inactive
+    gpioset> set GPIO24=1
+    gpioset> get
+    GPIO23=active GPIO24=active
+    gpioset> toggle
+    gpioset> get
+    GPIO23=inactive GPIO24=inactive
+    gpioset> toggle GPIO23
+    gpioset> get
+    GPIO23=active GPIO24=inactive
+    gpioset> exit
+
+    # Wait for three rising edge events on a single GPIO line, then exit.
+    $ gpiomon --num-events=3 --edges=rising GPIO22
+    10002.907638045    rising  "GPIO22"
+    10037.132562259    rising  "GPIO22"
+    10047.179790748    rising  "GPIO22"
+
+    # Wait for three edge events on a single GPIO line, with time in local time
+    # and with unquoted line name, then exit.
+    $ gpiomon --num-events=3 --edges=both --localtime --unquoted GPIO22
+    2022-11-15T10:36:59.109615508      rising  GPIO22
+    2022-11-15T10:36:59.129681898      falling GPIO22
+    2022-11-15T10:36:59.698971886      rising  GPIO22
+
+    # Wait for falling edge events with a custom output format.
+    $ gpiomon --format="%e %c %o %l %S" --edges=falling -c gpiochip0 22
+    2 gpiochip0 22 GPIO22 10946.693481859
+    2 gpiochip0 22 GPIO22 10947.025347604
+    2 gpiochip0 22 GPIO22 10947.283716669
+    2 gpiochip0 22 GPIO22 10947.570109430
+    ...
+
+    # Block until an edge event occurs. Don't print anything.
+    $ gpiomon --num-events=1 --quiet GPIO22
+
+    # Monitor multiple lines, exit after the first edge event.
+    $ gpiomon --quiet --num-events=1 GPIO5 GPIO6 GPIO12 GPIO17
+
+    # Monitor a line for changes to info.
+    $ gpionotify GPIO23
+    11571.816473718    requested       "GPIO23"
+    11571.816535124    released        "GPIO23"
+    11572.722894029    requested       "GPIO23"
+    11572.722932843    released        "GPIO23"
+    11573.222998598    requested       "GPIO23"
+    ...
+
+    # Monitor a line for requests, reporting UTC time and unquoted line name.
+     $ gpionotify --utc --unquoted GPIO23
+    2022-11-15T03:05:23.807090687Z     requested       GPIO23
+    2022-11-15T03:05:23.807151390Z     released        GPIO23
+    2022-11-15T03:05:24.784984280Z     requested       GPIO23
+    2022-11-15T03:05:24.785023873Z     released        GPIO23
+    ...
+
+    # Monitor multiple lines, exit after the first is requested.
+    $ gpionotify --quiet --num-events=1 --event=requested GPIO5 GPIO6 GPIO12 GPIO17
+
+    # Block until a line is released.
+    $ gpionotify --quiet --num-events=1 --event=released GPIO6
+
+BINDINGS
+--------
+
+High-level, object-oriented bindings for C++, python3 and Rust are provided.
+They can be enabled by passing --enable-bindings-cxx, --enable-bindings-python
+and --enable-bindings-rust arguments respectively to configure.
+
+C++ bindings require C++11 support and autoconf-archive collection if building
+from git.
+
+Python bindings require python3 support and libpython development files. Care
+must be taken when cross-compiling python bindings: users usually must specify
+the PYTHON_CPPFLAGS and PYTHON_LIBS variables in order to point the build
+system to the correct locations. During native builds, the configure script
+can auto-detect the location of the development files.
+
+Rust bindings require cargo support. When building the Rust bindings along the
+C library using make, they will be automatically configured to build against the
+build results of the C library.
+
+TESTING
+-------
+
+A comprehensive testing framework is included with the library and can be
+used to test both the core library code as well as the kernel-to-user-space
+interface.
+
+The minimum kernel version required to run the tests can be checked in the
+tests/gpiod-test.c source file (it's subject to change if new features are
+added to the kernel). The tests work together with the gpio-sim kernel module
+which must either be built-in or available for loading using kmod. A helper
+library - libgpiosim - is included to enable straightforward interaction with
+the module.
+
+To build the testing executable add the '--enable-tests' option when running
+the configure script. If enabled, the tests will be installed next to
+gpio-tools.
+
+As opposed to standard autotools projects, libgpiod doesn't execute any tests
+when invoking 'make check'. Instead the user must run them manually with
+superuser privileges.
+
+The testing framework uses the GLib unit testing library so development package
+for GLib must be installed.
+
+The gpio-tools programs can be tested separately using the gpio-tools-test.bash
+script. It requires shunit2[1] to run and assumes that the tested executables are
+in the same directory as the script.
+
+C++, Rust and Python bindings also include their own test-suites. All three
+reuse the libgpiosim library to avoid code duplication when interacting with
+gpio-sim.
+
+Python test-suite uses the standard unittest package. C++ tests use an external
+testing framework - Catch2 - which must be installed in the system. Rust
+bindings use the standard tests module layout and the #[test] attribute.
+
+DOCUMENTATION
+-------------
+
+All API symbols exposed by the core C API and C++ bindings are documented with
+doxygen markup blocks. Doxygen documentation can be generated by executing
+'make doc' given that the doxygen executable is available in the system.
+
+Python bindings contain help strings that can be accessed with the help
+builtin.
+
+Rust bindings use rustdoc.
+
+Man pages for command-line programs are generated automatically if gpio-tools
+were selected and help2man is available in the system.
+
+CONTRIBUTING
+------------
+
+Contributions are welcome - please send questions, patches and bug reports
+to the linux-gpio mailing list[2] by e-mailing to linux-gpio@vger.kernel.org
+(add the [libgpiod] prefix to the e-mail subject line).
+Note that the mailing list quietly drops HTML formatted e-mail, so be sure
+to send plain text[3].
+
+Code submissions should stick to the linux kernel coding style[4] and
+follow the kernel patch submission process[5] as applied to the libgpiod
+source tree.
+
+The mailing list archive[6] contains all the historical mails to the list,
+and is the place to check to ensure your e-mail has been received.
+Search for "libgpiod" to filter the list down to relevant messages.
+Those also provide examples of the expected formatting.
+Allow some time for your e-mail to propagate to the list before retrying,
+particularly if there are no e-mails in the list more recent than yours.
+
+[1] https://github.com/kward/shunit2
+[2] http://vger.kernel.org/vger-lists.html#linux-gpio
+[3] https://docs.kernel.org/process/email-clients.html
+[4] https://docs.kernel.org/process/coding-style.html
+[5] https://docs.kernel.org/process/submitting-patches.html
+[6] https://lore.kernel.org/linux-gpio/
diff --git a/TODO b/TODO
new file mode 100644 (file)
index 0000000..79a6246
--- /dev/null
+++ b/TODO
@@ -0,0 +1,53 @@
+# SPDX-License-Identifier: CC-BY-SA-4.0
+# SPDX-FileCopyrightText: 2017-2021 Bartosz Golaszewski <bartekgola@gmail.com>
+
+TODO list for libgpiod
+
+==========
+
+This document contains the list of things I'd like to have in libgpiod before
+declaring it "mostly feature-complete". If anyone wants to help, this can
+serve as the starting point.
+
+==========
+
+* implement dbus API for controlling GPIOs
+
+A common complaint from users about gpioset is that the state of a line is not
+retained once the program exits. While this is precisely the way linux
+character devices work, it's understandable that most users will want some
+centralized way of controlling GPIOs - similar to how sysfs worked.
+
+One of the possible solutions is a DBus API. We need a daemon exposing chips
+and lines as dbus objects and allowing to control and inspect lines using
+dbus methods and monitor them using signals.
+
+As of writing of this document some of the work has already been done and the
+skeleton of the dbus daemon written in C using GLib has already been developed
+and is partially functional.
+
+----------
+
+* implement a simple daemon for controlling GPIOs in C together with a client
+  program
+
+This is by far the lowest priority task. Similarly as with the dbus daemon:
+the goal is to provide a centralized agent controlling GPIOs with a simple
+interface consisting of a command line client communicating with the server
+over unix sockets.
+
+In this case however the goal is to have as few dependencies as possible. This
+is because for some small systems dbus is overkill. Since we won't be using any
+standardized protocol, it will take much more effort to implement it correctly.
+
+----------
+
+* improve gpioset --interactive tab completion
+
+The existing tab completion uses libedit's readline emulation layer which
+has a few limitations, including not being able to correctly handle quoted
+line names and being disabled when stdin/stdout are not a tty (which makes
+testing with gpio-tools-test.bash using coproc problematic).
+
+One approach that could address both these problems is to bypass the readline
+emulation and use the libedit API (histedit.h) directly.
diff --git a/autogen.sh b/autogen.sh
new file mode 100755 (executable)
index 0000000..420b821
--- /dev/null
@@ -0,0 +1,17 @@
+#!/bin/sh
+# SPDX-License-Identifier: GPL-2.0-or-later
+# SPDX-FileCopyrightText: 2017-2021 Bartosz Golaszewski <bartekgola@gmail.com>
+# SPDX-FileCopyrightText: 2017 Thierry Reding <treding@nvidia.com>
+
+srcdir=`dirname $0`
+test -z "$srcdir" && srcdir=.
+
+ORIGDIR=`pwd`
+cd "$srcdir"
+
+autoreconf --force --install --verbose || exit 1
+cd $ORIGDIR || exit $?
+
+if test -z "$NOCONFIGURE"; then
+       exec "$srcdir"/configure "$@"
+fi
diff --git a/bindings/Makefile.am b/bindings/Makefile.am
new file mode 100644 (file)
index 0000000..004ae23
--- /dev/null
@@ -0,0 +1,22 @@
+# SPDX-License-Identifier: GPL-2.0-or-later
+# SPDX-FileCopyrightText: 2017-2021 Bartosz Golaszewski <bartekgola@gmail.com>
+
+SUBDIRS = .
+
+if WITH_BINDINGS_CXX
+
+SUBDIRS += cxx
+
+endif
+
+if WITH_BINDINGS_PYTHON
+
+SUBDIRS += python
+
+endif
+
+if WITH_BINDINGS_RUST
+
+SUBDIRS += rust
+
+endif
diff --git a/bindings/cxx/Makefile.am b/bindings/cxx/Makefile.am
new file mode 100644 (file)
index 0000000..e2a89cf
--- /dev/null
@@ -0,0 +1,47 @@
+# SPDX-License-Identifier: GPL-2.0-or-later
+# SPDX-FileCopyrightText: 2017-2021 Bartosz Golaszewski <bartekgola@gmail.com>
+
+lib_LTLIBRARIES = libgpiodcxx.la
+libgpiodcxx_la_SOURCES = \
+       chip.cpp \
+       chip-info.cpp \
+       edge-event-buffer.cpp \
+       edge-event.cpp \
+       exception.cpp \
+       info-event.cpp \
+       internal.cpp \
+       internal.hpp \
+       line.cpp \
+       line-config.cpp \
+       line-info.cpp \
+       line-request.cpp \
+       line-settings.cpp \
+       misc.cpp \
+       request-builder.cpp \
+       request-config.cpp
+
+libgpiodcxx_la_CXXFLAGS = -Wall -Wextra -g -std=gnu++17
+libgpiodcxx_la_CXXFLAGS += -fvisibility=hidden -I$(top_srcdir)/include/
+libgpiodcxx_la_CXXFLAGS += $(PROFILING_CFLAGS)
+libgpiodcxx_la_LDFLAGS = -version-info $(subst .,:,$(ABI_CXX_VERSION))
+libgpiodcxx_la_LDFLAGS += $(PROFILING_LDFLAGS)
+libgpiodcxx_la_LIBADD = $(top_builddir)/lib/libgpiod.la
+
+include_HEADERS = gpiod.hpp
+
+pkgconfigdir = $(libdir)/pkgconfig
+pkgconfig_DATA = libgpiodcxx.pc
+
+SUBDIRS = gpiodcxx .
+
+if WITH_TESTS
+
+SUBDIRS += tests
+
+endif
+
+if WITH_EXAMPLES
+
+SUBDIRS += examples
+
+endif
diff --git a/bindings/cxx/chip-info.cpp b/bindings/cxx/chip-info.cpp
new file mode 100644 (file)
index 0000000..93dd6f5
--- /dev/null
@@ -0,0 +1,77 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// SPDX-FileCopyrightText: 2021-2022 Bartosz Golaszewski <brgl@bgdev.pl>
+
+#include <ostream>
+#include <utility>
+
+#include "internal.hpp"
+
+namespace gpiod {
+
+void chip_info::impl::set_info_ptr(chip_info_ptr& new_info)
+{
+       this->info = ::std::move(new_info);
+}
+
+GPIOD_CXX_API chip_info::chip_info()
+       : _m_priv(new impl)
+{
+
+}
+
+GPIOD_CXX_API chip_info::chip_info(const chip_info& other)
+       : _m_priv(other._m_priv)
+{
+
+}
+
+GPIOD_CXX_API chip_info::chip_info(chip_info&& other) noexcept
+       : _m_priv(::std::move(other._m_priv))
+{
+
+}
+
+GPIOD_CXX_API chip_info::~chip_info()
+{
+
+}
+
+GPIOD_CXX_API chip_info& chip_info::operator=(const chip_info& other)
+{
+       this->_m_priv = other._m_priv;
+
+       return *this;
+}
+
+GPIOD_CXX_API chip_info& chip_info::operator=(chip_info&& other) noexcept
+{
+       this->_m_priv = ::std::move(other._m_priv);
+
+       return *this;
+}
+
+GPIOD_CXX_API ::std::string chip_info::name() const noexcept
+{
+       return ::gpiod_chip_info_get_name(this->_m_priv->info.get());
+}
+
+GPIOD_CXX_API ::std::string chip_info::label() const noexcept
+{
+       return ::gpiod_chip_info_get_label(this->_m_priv->info.get());
+}
+
+GPIOD_CXX_API ::std::size_t chip_info::num_lines() const noexcept
+{
+       return ::gpiod_chip_info_get_num_lines(this->_m_priv->info.get());
+}
+
+GPIOD_CXX_API ::std::ostream& operator<<(::std::ostream& out, const chip_info& info)
+{
+       out << "gpiod::chip_info(name=\"" << info.name() <<
+              "\", label=\"" << info.label() <<
+              "\", num_lines=" << info.num_lines() << ")";
+
+       return out;
+}
+
+} /* namespace gpiod */
diff --git a/bindings/cxx/chip.cpp b/bindings/cxx/chip.cpp
new file mode 100644 (file)
index 0000000..8086839
--- /dev/null
@@ -0,0 +1,202 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// SPDX-FileCopyrightText: 2021-2022 Bartosz Golaszewski <brgl@bgdev.pl>
+
+#include <ostream>
+#include <utility>
+
+#include "internal.hpp"
+
+namespace gpiod {
+
+namespace {
+
+chip_ptr open_chip(const ::std::filesystem::path& path)
+{
+       chip_ptr chip(::gpiod_chip_open(path.c_str()));
+       if (!chip)
+               throw_from_errno("unable to open the GPIO device " + path.string());
+
+       return chip;
+}
+
+} /* namespace */
+
+chip::impl::impl(const ::std::filesystem::path& path)
+       : chip(open_chip(path))
+{
+
+}
+
+void chip::impl::throw_if_closed() const
+{
+       if (!this->chip)
+               throw chip_closed("GPIO chip has been closed");
+}
+
+GPIOD_CXX_API chip::chip(const ::std::filesystem::path& path)
+       : _m_priv(new impl(path))
+{
+
+}
+
+chip::chip(const chip& other)
+       : _m_priv(other._m_priv)
+{
+
+}
+
+GPIOD_CXX_API chip::chip(chip&& other) noexcept
+       : _m_priv(::std::move(other._m_priv))
+{
+
+}
+
+GPIOD_CXX_API chip::~chip()
+{
+
+}
+
+GPIOD_CXX_API chip& chip::operator=(chip&& other) noexcept
+{
+       this->_m_priv = ::std::move(other._m_priv);
+
+       return *this;
+}
+
+GPIOD_CXX_API chip::operator bool() const noexcept
+{
+       return this->_m_priv->chip.get() != nullptr;
+}
+
+GPIOD_CXX_API void chip::close()
+{
+       this->_m_priv->throw_if_closed();
+
+       this->_m_priv->chip.reset();
+}
+
+GPIOD_CXX_API ::std::filesystem::path chip::path() const
+{
+       this->_m_priv->throw_if_closed();
+
+       return ::gpiod_chip_get_path(this->_m_priv->chip.get());
+}
+
+GPIOD_CXX_API chip_info chip::get_info() const
+{
+       this->_m_priv->throw_if_closed();
+
+       chip_info_ptr info(::gpiod_chip_get_info(this->_m_priv->chip.get()));
+       if (!info)
+               throw_from_errno("failed to retrieve GPIO chip info");
+
+       chip_info ret;
+
+       ret._m_priv->set_info_ptr(info);
+
+       return ret;
+}
+
+GPIOD_CXX_API line_info chip::get_line_info(line::offset offset) const
+{
+       this->_m_priv->throw_if_closed();
+
+       line_info_ptr info(::gpiod_chip_get_line_info(this->_m_priv->chip.get(), offset));
+       if (!info)
+               throw_from_errno("unable to retrieve GPIO line info");
+
+       line_info ret;
+
+       ret._m_priv->set_info_ptr(info);
+
+       return ret;
+}
+
+GPIOD_CXX_API line_info chip::watch_line_info(line::offset offset) const
+{
+       this->_m_priv->throw_if_closed();
+
+       line_info_ptr info(::gpiod_chip_watch_line_info(this->_m_priv->chip.get(), offset));
+       if (!info)
+               throw_from_errno("unable to start watching GPIO line info changes");
+
+       line_info ret;
+
+       ret._m_priv->set_info_ptr(info);
+
+       return ret;
+}
+
+GPIOD_CXX_API void chip::unwatch_line_info(line::offset offset) const
+{
+       this->_m_priv->throw_if_closed();
+
+       int ret = ::gpiod_chip_unwatch_line_info(this->_m_priv->chip.get(), offset);
+       if (ret)
+               throw_from_errno("unable to unwatch line status changes");
+}
+
+GPIOD_CXX_API int chip::fd() const
+{
+       this->_m_priv->throw_if_closed();
+
+       return ::gpiod_chip_get_fd(this->_m_priv->chip.get());
+}
+
+GPIOD_CXX_API bool chip::wait_info_event(const ::std::chrono::nanoseconds& timeout) const
+{
+       this->_m_priv->throw_if_closed();
+
+       int ret = ::gpiod_chip_wait_info_event(this->_m_priv->chip.get(), timeout.count());
+       if (ret < 0)
+               throw_from_errno("error waiting for info events");
+
+       return ret;
+}
+
+GPIOD_CXX_API info_event chip::read_info_event() const
+{
+       this->_m_priv->throw_if_closed();
+
+       info_event_ptr event(gpiod_chip_read_info_event(this->_m_priv->chip.get()));
+       if (!event)
+               throw_from_errno("error reading the line info event_handle");
+
+       info_event ret;
+       ret._m_priv->set_info_event_ptr(event);
+
+       return ret;
+}
+
+GPIOD_CXX_API int chip::get_line_offset_from_name(const ::std::string& name) const
+{
+       this->_m_priv->throw_if_closed();
+
+       int ret = ::gpiod_chip_get_line_offset_from_name(this->_m_priv->chip.get(), name.c_str());
+       if (ret < 0) {
+               if (errno == ENOENT)
+                       return -1;
+
+               throw_from_errno("error looking up line by name");
+       }
+
+       return ret;
+}
+
+GPIOD_CXX_API request_builder chip::prepare_request()
+{
+       return request_builder(*this);
+}
+
+GPIOD_CXX_API ::std::ostream& operator<<(::std::ostream& out, const chip& chip)
+{
+       if (!chip)
+               out << "gpiod::chip(closed)";
+       else
+               out << "gpiod::chip(path=" << chip.path() <<
+                      ", info=" << chip.get_info() << ")";
+
+       return out;
+}
+
+} /* namespace gpiod */
diff --git a/bindings/cxx/edge-event-buffer.cpp b/bindings/cxx/edge-event-buffer.cpp
new file mode 100644 (file)
index 0000000..0d5cb36
--- /dev/null
@@ -0,0 +1,117 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// SPDX-FileCopyrightText: 2021-2022 Bartosz Golaszewski <brgl@bgdev.pl>
+
+#include <iterator>
+#include <ostream>
+#include <utility>
+
+#include "internal.hpp"
+
+namespace gpiod {
+
+namespace {
+
+edge_event_buffer_ptr make_edge_event_buffer(unsigned int capacity)
+{
+       edge_event_buffer_ptr buffer(::gpiod_edge_event_buffer_new(capacity));
+       if (!buffer)
+               throw_from_errno("unable to allocate the edge event buffer");
+
+       return buffer;
+}
+
+} /* namespace */
+
+edge_event_buffer::impl::impl(unsigned int capacity)
+       : buffer(make_edge_event_buffer(capacity)),
+         events()
+{
+       events.reserve(capacity);
+
+       for (unsigned int i = 0; i < capacity; i++) {
+               events.push_back(edge_event());
+               events.back()._m_priv.reset(new edge_event::impl_external);
+       }
+}
+
+int edge_event_buffer::impl::read_events(const line_request_ptr& request, unsigned int max_events)
+{
+       int ret = ::gpiod_line_request_read_edge_events(request.get(),
+                                                      this->buffer.get(), max_events);
+       if (ret < 0)
+               throw_from_errno("error reading edge events from file descriptor");
+
+       for (int i = 0; i < ret; i++) {
+               ::gpiod_edge_event* event = ::gpiod_edge_event_buffer_get_event(this->buffer.get(), i);
+
+               dynamic_cast<edge_event::impl_external&>(*this->events[i]._m_priv).event = event;
+       }
+
+       return ret;
+}
+
+GPIOD_CXX_API edge_event_buffer::edge_event_buffer(::std::size_t capacity)
+       : _m_priv(new impl(capacity))
+{
+
+}
+
+GPIOD_CXX_API edge_event_buffer::edge_event_buffer(edge_event_buffer&& other) noexcept
+       : _m_priv(::std::move(other._m_priv))
+{
+
+}
+
+GPIOD_CXX_API edge_event_buffer::~edge_event_buffer()
+{
+
+}
+
+GPIOD_CXX_API edge_event_buffer& edge_event_buffer::operator=(edge_event_buffer&& other) noexcept
+{
+       this->_m_priv = ::std::move(other._m_priv);
+
+       return *this;
+}
+
+GPIOD_CXX_API const edge_event& edge_event_buffer::get_event(unsigned int index) const
+{
+       return this->_m_priv->events.at(index);
+}
+
+GPIOD_CXX_API ::std::size_t edge_event_buffer::num_events() const
+{
+       return ::gpiod_edge_event_buffer_get_num_events(this->_m_priv->buffer.get());
+}
+
+GPIOD_CXX_API ::std::size_t edge_event_buffer::capacity() const noexcept
+{
+       return ::gpiod_edge_event_buffer_get_capacity(this->_m_priv->buffer.get());
+}
+
+GPIOD_CXX_API edge_event_buffer::const_iterator edge_event_buffer::begin() const noexcept
+{
+       return this->_m_priv->events.begin();
+}
+
+GPIOD_CXX_API edge_event_buffer::const_iterator edge_event_buffer::end() const noexcept
+{
+       return this->_m_priv->events.begin() + this->num_events();
+}
+
+GPIOD_CXX_API ::std::ostream& operator<<(::std::ostream& out, const edge_event_buffer& buf)
+{
+       out << "gpiod::edge_event_buffer(num_events=" << buf.num_events() <<
+              ", capacity=" << buf.capacity() <<
+              ", events=[";
+
+       ::std::copy(buf.begin(), ::std::prev(buf.end()),
+                   ::std::ostream_iterator<edge_event>(out, ", "));
+       out << *(::std::prev(buf.end()));
+
+       out << "])";
+
+       return out;
+}
+
+} /* namespace gpiod */
diff --git a/bindings/cxx/edge-event.cpp b/bindings/cxx/edge-event.cpp
new file mode 100644 (file)
index 0000000..9ef311c
--- /dev/null
@@ -0,0 +1,137 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// SPDX-FileCopyrightText: 2021-2022 Bartosz Golaszewski <brgl@bgdev.pl>
+
+#include <map>
+#include <ostream>
+#include <utility>
+
+#include "internal.hpp"
+
+namespace gpiod {
+
+namespace {
+
+const ::std::map<int, edge_event::event_type> event_type_mapping = {
+       { GPIOD_EDGE_EVENT_RISING_EDGE,         edge_event::event_type::RISING_EDGE },
+       { GPIOD_EDGE_EVENT_FALLING_EDGE,        edge_event::event_type::FALLING_EDGE },
+};
+
+const ::std::map<edge_event::event_type, ::std::string> event_type_names = {
+       { edge_event::event_type::RISING_EDGE,          "RISING_EDGE" },
+       { edge_event::event_type::FALLING_EDGE,         "FALLING_EDGE" },
+};
+
+} /* namespace */
+
+::gpiod_edge_event* edge_event::impl_managed::get_event_ptr() const noexcept
+{
+       return this->event.get();
+}
+
+::std::shared_ptr<edge_event::impl>
+edge_event::impl_managed::copy(const ::std::shared_ptr<impl>& self) const
+{
+       return self;
+}
+
+edge_event::impl_external::impl_external()
+       : impl(),
+         event(nullptr)
+{
+
+}
+
+::gpiod_edge_event* edge_event::impl_external::get_event_ptr() const noexcept
+{
+       return this->event;
+}
+
+::std::shared_ptr<edge_event::impl>
+edge_event::impl_external::copy([[maybe_unused]] const ::std::shared_ptr<impl>& self) const
+{
+       ::std::shared_ptr<impl> ret(new impl_managed);
+       impl_managed& managed = dynamic_cast<impl_managed&>(*ret);
+
+       managed.event.reset(::gpiod_edge_event_copy(this->event));
+       if (!managed.event)
+               throw_from_errno("unable to copy the edge event object");
+
+       return ret;
+}
+
+edge_event::edge_event()
+       : _m_priv()
+{
+
+}
+
+GPIOD_CXX_API edge_event::edge_event(const edge_event& other)
+       : _m_priv(other._m_priv->copy(other._m_priv))
+{
+
+}
+
+GPIOD_CXX_API edge_event::edge_event(edge_event&& other) noexcept
+       : _m_priv(::std::move(other._m_priv))
+{
+
+}
+
+GPIOD_CXX_API edge_event::~edge_event()
+{
+
+}
+
+GPIOD_CXX_API edge_event& edge_event::operator=(const edge_event& other)
+{
+       this->_m_priv = other._m_priv->copy(other._m_priv);
+
+       return *this;
+}
+
+GPIOD_CXX_API edge_event& edge_event::operator=(edge_event&& other) noexcept
+{
+       this->_m_priv = ::std::move(other._m_priv);
+
+       return *this;
+}
+
+GPIOD_CXX_API edge_event::event_type edge_event::type() const
+{
+       int evtype = ::gpiod_edge_event_get_event_type(this->_m_priv->get_event_ptr());
+
+       return get_mapped_value(evtype, event_type_mapping);
+}
+
+GPIOD_CXX_API timestamp edge_event::timestamp_ns() const noexcept
+{
+       return ::gpiod_edge_event_get_timestamp_ns(this->_m_priv->get_event_ptr());
+}
+
+GPIOD_CXX_API line::offset edge_event::line_offset() const noexcept
+{
+       return ::gpiod_edge_event_get_line_offset(this->_m_priv->get_event_ptr());
+}
+
+GPIOD_CXX_API unsigned long edge_event::global_seqno() const noexcept
+{
+       return ::gpiod_edge_event_get_global_seqno(this->_m_priv->get_event_ptr());
+}
+
+GPIOD_CXX_API unsigned long edge_event::line_seqno() const noexcept
+{
+       return ::gpiod_edge_event_get_line_seqno(this->_m_priv->get_event_ptr());
+}
+
+GPIOD_CXX_API ::std::ostream& operator<<(::std::ostream& out, const edge_event& event)
+{
+       out << "gpiod::edge_event(type='" << event_type_names.at(event.type()) <<
+              "', timestamp=" << event.timestamp_ns() <<
+              ", line_offset=" << event.line_offset() <<
+              ", global_seqno=" << event.global_seqno() <<
+              ", line_seqno=" << event.line_seqno() << ")";
+
+       return out;
+}
+
+} /* namespace gpiod */
diff --git a/bindings/cxx/examples/.gitignore b/bindings/cxx/examples/.gitignore
new file mode 100644 (file)
index 0000000..0f9b39e
--- /dev/null
@@ -0,0 +1,16 @@
+# SPDX-License-Identifier: GPL-2.0-or-later
+# SPDX-FileCopyrightText: 2017-2021 Bartosz Golaszewski <bartekgola@gmail.com>
+
+async_watch_line_value
+find_line_by_name
+get_chip_info
+get_line_info
+get_line_value
+get_multiple_line_values
+reconfigure_input_to_output
+toggle_line_value
+toggle_multiple_line_values
+watch_line_info
+watch_line_rising
+watch_line_value
+watch_multiple_line_values
diff --git a/bindings/cxx/examples/Makefile.am b/bindings/cxx/examples/Makefile.am
new file mode 100644 (file)
index 0000000..eca4d64
--- /dev/null
@@ -0,0 +1,47 @@
+# SPDX-License-Identifier: GPL-2.0-or-later
+# SPDX-FileCopyrightText: 2017-2021 Bartosz Golaszewski <bartekgola@gmail.com>
+
+AM_CXXFLAGS = -I$(top_srcdir)/bindings/cxx/ -I$(top_srcdir)/include
+AM_CXXFLAGS += -Wall -Wextra -g -std=gnu++17
+LDADD = $(top_builddir)/bindings/cxx/libgpiodcxx.la
+
+noinst_PROGRAMS = \
+       async_watch_line_value \
+       find_line_by_name \
+       get_chip_info \
+       get_line_info \
+       get_line_value \
+       get_multiple_line_values \
+       reconfigure_input_to_output \
+       toggle_line_value \
+       toggle_multiple_line_values \
+       watch_line_info \
+       watch_line_rising \
+       watch_line_value \
+       watch_multiple_line_values
+
+async_watch_line_value_SOURCES = async_watch_line_value.cpp
+
+find_line_by_name_SOURCES = find_line_by_name.cpp
+
+get_chip_info_SOURCES = get_chip_info.cpp
+
+get_line_info_SOURCES = get_line_info.cpp
+
+get_line_value_SOURCES = get_line_value.cpp
+
+get_multiple_line_values_SOURCES = get_multiple_line_values.cpp
+
+reconfigure_input_to_output_SOURCES = reconfigure_input_to_output.cpp
+
+toggle_line_value_SOURCES = toggle_line_value.cpp
+
+toggle_multiple_line_values_SOURCES = toggle_multiple_line_values.cpp
+
+watch_line_info_SOURCES = watch_line_info.cpp
+
+watch_line_value_SOURCES = watch_line_value.cpp
+
+watch_line_rising_SOURCES = watch_line_rising.cpp
+
+watch_multiple_line_values_SOURCES = watch_multiple_line_values.cpp
diff --git a/bindings/cxx/examples/async_watch_line_value.cpp b/bindings/cxx/examples/async_watch_line_value.cpp
new file mode 100644 (file)
index 0000000..9ea9763
--- /dev/null
@@ -0,0 +1,88 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+// SPDX-FileCopyrightText: 2023 Kent Gibson <warthog618@gmail.com>
+
+/* Minimal example of asynchronously watching for edges on a single line. */
+
+#include <cstdlib>
+#include <cstring>
+#include <filesystem>
+#include <gpiod.hpp>
+#include <iomanip>
+#include <iostream>
+#include <poll.h>
+
+namespace {
+
+/* Example configuration - customize to suit your situation. */
+const ::std::filesystem::path chip_path("/dev/gpiochip0");
+const ::gpiod::line::offset line_offset = 5;
+
+const char* edge_event_type_str(const ::gpiod::edge_event &event)
+{
+       switch (event.type()) {
+       case ::gpiod::edge_event::event_type::RISING_EDGE:
+               return "Rising ";
+       case ::gpiod::edge_event::event_type::FALLING_EDGE:
+               return "Falling";
+       default:
+               return "Unknown";
+       }
+}
+
+} /* namespace */
+
+int main()
+{
+       /*
+        * Assume a button connecting the pin to ground, so pull it up and
+        * provide some debounce.
+        */
+       auto request =
+               ::gpiod::chip(chip_path)
+                       .prepare_request()
+                       .set_consumer("async-watch-line-value")
+                       .add_line_settings(
+                               line_offset,
+                               ::gpiod::line_settings()
+                                       .set_direction(
+                                               ::gpiod::line::direction::INPUT)
+                                       .set_edge_detection(
+                                               ::gpiod::line::edge::BOTH)
+                                       .set_bias(::gpiod::line::bias::PULL_UP)
+                                       .set_debounce_period(
+                                               std::chrono::milliseconds(10)))
+                       .do_request();
+
+       /*
+        * A larger buffer is an optimisation for reading bursts of events from
+        * the kernel, but that is not necessary in this case, so 1 is fine.
+        */
+       ::gpiod::edge_event_buffer buffer(1);
+
+       struct pollfd pollfd;
+       pollfd.fd = request.fd();
+       pollfd.events = POLLIN;
+
+       for (;;) {
+               /*
+                * Other fds could be registered with the poll and be handled
+                * separately using the pollfd.revents after poll()
+                */
+               auto ret = poll(&pollfd, 1, -1);
+               if (ret == -1) {
+                       ::std::cerr << "error waiting for edge events: "
+                                   << strerror(errno) << ::std::endl;
+
+                       return EXIT_FAILURE;
+               }
+
+               request.read_edge_events(buffer);
+
+               for (const auto& event : buffer)
+                       ::std::cout << "offset: " << event.line_offset()
+                                   << "  type: " << ::std::setw(7)
+                                   << ::std::left << edge_event_type_str(event)
+                                   << "  event #" << event.line_seqno()
+                                   << ::std::endl;
+       }
+}
diff --git a/bindings/cxx/examples/find_line_by_name.cpp b/bindings/cxx/examples/find_line_by_name.cpp
new file mode 100644 (file)
index 0000000..7b56e69
--- /dev/null
@@ -0,0 +1,41 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+// SPDX-FileCopyrightText: 2023 Kent Gibson <warthog618@gmail.com>
+
+/* Minimal example of finding a line with the given name. */
+
+#include <cstdlib>
+#include <filesystem>
+#include <gpiod.hpp>
+#include <iostream>
+
+namespace {
+
+/* Example configuration - customize to suit your situation */
+const ::std::string line_name = "GPIO19";
+
+} /* namespace */
+
+int main()
+{
+       /*
+        * Names are not guaranteed unique, so this finds the first line with
+        * the given name.
+        */
+       for (const auto &entry :
+            ::std::filesystem::directory_iterator("/dev/")) {
+               if (::gpiod::is_gpiochip_device(entry.path())) {
+                       ::gpiod::chip chip(entry.path());
+
+                       auto offset = chip.get_line_offset_from_name(line_name);
+                       if (offset >= 0) {
+                               ::std::cout << line_name << ": "
+                                           << chip.get_info().name() << " "
+                                           << offset << ::std::endl;
+                               return EXIT_SUCCESS;
+                       }
+               }
+       }
+       ::std::cout << "line '" << line_name << "' not found" << ::std::endl;
+
+       return EXIT_FAILURE;
+}
diff --git a/bindings/cxx/examples/get_chip_info.cpp b/bindings/cxx/examples/get_chip_info.cpp
new file mode 100644 (file)
index 0000000..2bf26f0
--- /dev/null
@@ -0,0 +1,27 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+// SPDX-FileCopyrightText: 2023 Kent Gibson <warthog618@gmail.com>
+
+/* Minimal example of reading the info for a chip. */
+
+#include <cstdlib>
+#include <filesystem>
+#include <gpiod.hpp>
+#include <iostream>
+
+namespace {
+
+/* Example configuration - customize to suit your situation */
+const ::std::filesystem::path chip_path("/dev/gpiochip0");
+
+} /* namespace */
+
+int main()
+{
+       ::gpiod::chip chip(chip_path);
+       auto info = chip.get_info();
+
+       ::std::cout << info.name() << " [" << info.label() << "] ("
+                   << info.num_lines() << " lines)" << ::std::endl;
+
+       return EXIT_SUCCESS;
+}
diff --git a/bindings/cxx/examples/get_line_info.cpp b/bindings/cxx/examples/get_line_info.cpp
new file mode 100644 (file)
index 0000000..7d517af
--- /dev/null
@@ -0,0 +1,39 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+// SPDX-FileCopyrightText: 2023 Kent Gibson <warthog618@gmail.com>
+
+/* Minimal example of reading the info for a line. */
+
+#include <cstdlib>
+#include <filesystem>
+#include <gpiod.hpp>
+#include <iomanip>
+#include <iostream>
+
+namespace {
+
+/* Example configuration - customize to suit your situation */
+const ::std::filesystem::path chip_path("/dev/gpiochip0");
+const ::gpiod::line::offset line_offset = 3;
+
+} /* namespace */
+
+int main()
+{
+       auto chip = ::gpiod::chip(chip_path);
+       auto info = chip.get_line_info(line_offset);
+
+       ::std::cout << "line " << ::std::setw(3) << info.offset() << ": "
+                   << ::std::setw(12)
+                   << (info.name().empty() ? "unnamed" : info.name()) << " "
+                   << ::std::setw(12)
+                   << (info.consumer().empty() ? "unused" : info.consumer())
+                   << " " << ::std::setw(8)
+                   << (info.direction() == ::gpiod::line::direction::INPUT ?
+                               "input" :
+                               "output")
+                   << " " << ::std::setw(10)
+                   << (info.active_low() ? "active-low" : "active-high")
+                   << ::std::endl;
+
+       return EXIT_SUCCESS;
+}
diff --git a/bindings/cxx/examples/get_line_value.cpp b/bindings/cxx/examples/get_line_value.cpp
new file mode 100644 (file)
index 0000000..fe4f52e
--- /dev/null
@@ -0,0 +1,38 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+// SPDX-FileCopyrightText: 2023 Kent Gibson <warthog618@gmail.com>
+
+/* Minimal example of reading a single line. */
+
+#include <cstdlib>
+#include <filesystem>
+#include <gpiod.hpp>
+#include <iostream>
+
+namespace {
+
+/* Example configuration - customize to suit your situation */
+const ::std::filesystem::path chip_path("/dev/gpiochip0");
+const ::gpiod::line::offset line_offset = 5;
+
+} /* namespace */
+
+int main()
+{
+       auto request = ::gpiod::chip(chip_path)
+                              .prepare_request()
+                              .set_consumer("get-line-value")
+                              .add_line_settings(
+                                      line_offset,
+                                      ::gpiod::line_settings().set_direction(
+                                              ::gpiod::line::direction::INPUT))
+                              .do_request();
+
+       ::std::cout << line_offset << "="
+                   << (request.get_value(line_offset) ==
+                                       ::gpiod::line::value::ACTIVE ?
+                               "Active" :
+                               "Inactive")
+                   << ::std::endl;
+
+       return EXIT_SUCCESS;
+}
diff --git a/bindings/cxx/examples/get_multiple_line_values.cpp b/bindings/cxx/examples/get_multiple_line_values.cpp
new file mode 100644 (file)
index 0000000..cbd5395
--- /dev/null
@@ -0,0 +1,40 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+// SPDX-FileCopyrightText: 2023 Kent Gibson <warthog618@gmail.com>
+
+/* Minimal example of reading multiple lines. */
+
+#include <cstdlib>
+#include <gpiod.hpp>
+#include <iostream>
+
+namespace {
+
+/* Example configuration - customize to suit your situation */
+const ::std::filesystem::path chip_path("/dev/gpiochip0");
+const ::gpiod::line::offsets line_offsets = { 5, 3, 7 };
+
+} /* namespace */
+
+int main()
+{
+       auto request = ::gpiod::chip(chip_path)
+                              .prepare_request()
+                              .set_consumer("get-multiple-line-values")
+                              .add_line_settings(
+                                      line_offsets,
+                                      ::gpiod::line_settings().set_direction(
+                                              ::gpiod::line::direction::INPUT))
+                              .do_request();
+
+       auto values = request.get_values();
+
+       for (size_t i = 0; i < line_offsets.size(); i++)
+               ::std::cout << line_offsets[i] << "="
+                           << (values[i] == ::gpiod::line::value::ACTIVE ?
+                                       "Active" :
+                                       "Inactive")
+                           << ' ';
+       ::std::cout << ::std::endl;
+
+       return EXIT_SUCCESS;
+}
diff --git a/bindings/cxx/examples/reconfigure_input_to_output.cpp b/bindings/cxx/examples/reconfigure_input_to_output.cpp
new file mode 100644 (file)
index 0000000..d55eaf5
--- /dev/null
@@ -0,0 +1,58 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+// SPDX-FileCopyrightText: 2023 Kent Gibson <warthog618@gmail.com>
+
+/*
+ * Example of a bi-directional line requested as input and then switched
+ * to output.
+ */
+
+#include <cstdlib>
+#include <filesystem>
+#include <gpiod.hpp>
+#include <iostream>
+
+namespace {
+
+/* Example configuration - customize to suit your situation */
+const ::std::filesystem::path chip_path("/dev/gpiochip0");
+const ::gpiod::line::offset line_offset = 5;
+
+} /* namespace */
+
+int main()
+{
+       /* request the line initially as an input */
+       auto request = ::gpiod::chip(chip_path)
+                              .prepare_request()
+                              .set_consumer("reconfigure-input-to-output")
+                              .add_line_settings(
+                                      line_offset,
+                                      ::gpiod::line_settings().set_direction(
+                                              ::gpiod::line::direction::INPUT))
+                              .do_request();
+
+       /* read the current line value */
+       ::std::cout << line_offset << "="
+                   << (request.get_value(line_offset) ==
+                                       ::gpiod::line::value::ACTIVE ?
+                               "Active" :
+                               "Inactive")
+                   << " (input)" << ::std::endl;
+
+       /* switch the line to an output and drive it low */
+       request.reconfigure_lines(::gpiod::line_config().add_line_settings(
+               line_offset,
+               ::gpiod::line_settings()
+                       .set_direction(::gpiod::line::direction::OUTPUT)
+                       .set_output_value(::gpiod::line::value::INACTIVE)));
+
+       /* report the current driven value */
+       ::std::cout << line_offset << "="
+                   << (request.get_value(line_offset) ==
+                                       ::gpiod::line::value::ACTIVE ?
+                               "Active" :
+                               "Inactive")
+                   << " (output)" << ::std::endl;
+
+       return EXIT_SUCCESS;
+}
diff --git a/bindings/cxx/examples/toggle_line_value.cpp b/bindings/cxx/examples/toggle_line_value.cpp
new file mode 100644 (file)
index 0000000..3dbdb71
--- /dev/null
@@ -0,0 +1,49 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+// SPDX-FileCopyrightText: 2023 Kent Gibson <warthog618@gmail.com>
+
+/* Minimal example of toggling a single line. */
+
+#include <cstdlib>
+#include <chrono>
+#include <filesystem>
+#include <gpiod.hpp>
+#include <iostream>
+#include <thread>
+
+namespace {
+
+/* Example configuration - customize to suit your situation. */
+const ::std::filesystem::path chip_path("/dev/gpiochip0");
+const ::gpiod::line::offset line_offset = 5;
+
+::gpiod::line::value toggle_value(::gpiod::line::value v)
+{
+       return (v == ::gpiod::line::value::ACTIVE) ?
+                      ::gpiod::line::value::INACTIVE :
+                      ::gpiod::line::value::ACTIVE;
+}
+
+} /* namespace */
+
+int main()
+{
+       ::gpiod::line::value value = ::gpiod::line::value::ACTIVE;
+
+       auto request =
+               ::gpiod::chip(chip_path)
+                       .prepare_request()
+                       .set_consumer("toggle-line-value")
+                       .add_line_settings(
+                               line_offset,
+                               ::gpiod::line_settings().set_direction(
+                                       ::gpiod::line::direction::OUTPUT))
+                       .do_request();
+
+       for (;;) {
+               ::std::cout << line_offset << "=" << value << ::std::endl;
+
+               std::this_thread::sleep_for(std::chrono::seconds(1));
+               value = toggle_value(value);
+               request.set_value(line_offset, value);
+       }
+}
diff --git a/bindings/cxx/examples/toggle_multiple_line_values.cpp b/bindings/cxx/examples/toggle_multiple_line_values.cpp
new file mode 100644 (file)
index 0000000..df55313
--- /dev/null
@@ -0,0 +1,63 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+// SPDX-FileCopyrightText: 2023 Kent Gibson <warthog618@gmail.com>
+
+/* Minimal example of toggling multiple lines. */
+
+#include <cstdlib>
+#include <gpiod.hpp>
+#include <iostream>
+#include <thread>
+
+namespace {
+
+/* Example configuration - customize to suit your situation */
+const ::std::filesystem::path chip_path("/dev/gpiochip0");
+const ::gpiod::line::offsets line_offsets = { 5, 3, 7 };
+
+::gpiod::line::value toggle_value(::gpiod::line::value v)
+{
+       return (v == ::gpiod::line::value::ACTIVE) ?
+                      ::gpiod::line::value::INACTIVE :
+                      ::gpiod::line::value::ACTIVE;
+}
+
+void toggle_values(::gpiod::line::values &values)
+{
+       for (size_t i = 0; i < values.size(); i++)
+               values[i] = toggle_value(values[i]);
+}
+
+void print_values(::gpiod::line::offsets const &offsets,
+                 ::gpiod::line::values const &values)
+{
+       for (size_t i = 0; i < offsets.size(); i++)
+               ::std::cout << offsets[i] << "=" << values[i] << ' ';
+       ::std::cout << ::std::endl;
+}
+
+} /* namespace */
+
+int main()
+{
+       ::gpiod::line::values values = { ::gpiod::line::value::ACTIVE,
+                                        ::gpiod::line::value::ACTIVE,
+                                        ::gpiod::line::value::INACTIVE };
+
+       auto request =
+               ::gpiod::chip(chip_path)
+                       .prepare_request()
+                       .set_consumer("toggle-multiple-line-values")
+                       .add_line_settings(
+                               line_offsets,
+                               ::gpiod::line_settings().set_direction(
+                                       ::gpiod::line::direction::OUTPUT))
+                       .set_output_values(values)
+                       .do_request();
+
+       for (;;) {
+               print_values(line_offsets, values);
+               std::this_thread::sleep_for(std::chrono::seconds(1));
+               toggle_values(values);
+               request.set_values(line_offsets, values);
+       }
+}
diff --git a/bindings/cxx/examples/watch_line_info.cpp b/bindings/cxx/examples/watch_line_info.cpp
new file mode 100644 (file)
index 0000000..6d55500
--- /dev/null
@@ -0,0 +1,49 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+// SPDX-FileCopyrightText: 2023 Kent Gibson <warthog618@gmail.com>
+
+/* Minimal example of watching for requests on particular lines. */
+
+#include <cstdlib>
+#include <gpiod.hpp>
+#include <iomanip>
+#include <iostream>
+
+namespace {
+
+/* Example configuration - customize to suit your situation. */
+const ::std::filesystem::path chip_path("/dev/gpiochip0");
+const ::gpiod::line::offsets line_offsets = { 5, 3, 7 };
+
+const char *event_type(const ::gpiod::info_event &event)
+{
+       switch (event.type()) {
+       case ::gpiod::info_event::event_type::LINE_REQUESTED:
+               return "Requested";
+       case ::gpiod::info_event::event_type::LINE_RELEASED:
+               return "Released";
+       case ::gpiod::info_event::event_type::LINE_CONFIG_CHANGED:
+               return "Reconfig";
+       default:
+               return "Unknown";
+       }
+}
+
+} /* namespace */
+
+int main()
+{
+       ::gpiod::chip chip(chip_path);
+
+       for (auto offset :line_offsets)
+               chip.watch_line_info(offset);
+
+       for (;;) {
+               /* Blocks until at least one event is available */
+               auto event = chip.read_info_event();
+               ::std::cout << "line: " << event.get_line_info().offset() << " "
+                           << ::std::setw(9) << ::std::left
+                           << event_type(event) << " "
+                           << event.timestamp_ns() / 1000000000 << "."
+                           << event.timestamp_ns() % 1000000000 << ::std::endl;
+       }
+}
diff --git a/bindings/cxx/examples/watch_line_rising.cpp b/bindings/cxx/examples/watch_line_rising.cpp
new file mode 100644 (file)
index 0000000..33e4f01
--- /dev/null
@@ -0,0 +1,64 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+// SPDX-FileCopyrightText: 2023 Kent Gibson <warthog618@gmail.com>
+
+/* Minimal example of watching for rising edges on a single line. */
+
+#include <cstdlib>
+#include <filesystem>
+#include <gpiod.hpp>
+#include <iomanip>
+#include <iostream>
+
+namespace {
+
+/* Example configuration - customize to suit your situation. */
+const ::std::filesystem::path chip_path("/dev/gpiochip0");
+const ::gpiod::line::offset line_offset = 5;
+
+const char *edge_event_type_str(const ::gpiod::edge_event &event)
+{
+       switch (event.type()) {
+       case ::gpiod::edge_event::event_type::RISING_EDGE:
+               return "Rising";
+       case ::gpiod::edge_event::event_type::FALLING_EDGE:
+               return "Falling";
+       default:
+               return "Unknown";
+       }
+}
+
+} /* namespace */
+
+int main()
+{
+       auto request =
+               ::gpiod::chip(chip_path)
+                       .prepare_request()
+                       .set_consumer("watch-line-value")
+                       .add_line_settings(
+                               line_offset,
+                               ::gpiod::line_settings()
+                                       .set_direction(
+                                               ::gpiod::line::direction::INPUT)
+                                       .set_edge_detection(
+                                               ::gpiod::line::edge::RISING)
+                       )
+                       .do_request();
+
+       /*
+        * A larger buffer is an optimisation for reading bursts of events from
+        * the kernel, but that is not necessary in this case, so 1 is fine.
+        */
+       ::gpiod::edge_event_buffer buffer(1);
+
+       for (;;) {
+               /* Blocks until at least one event is available. */
+               request.read_edge_events(buffer);
+
+               for (const auto &event : buffer)
+                       ::std::cout << "line: " << event.line_offset()
+                                   << "  type: " << ::std::setw(7) << ::std::left << edge_event_type_str(event)
+                                   << "  event #" << event.line_seqno()
+                                   << ::std::endl;
+       }
+}
diff --git a/bindings/cxx/examples/watch_line_value.cpp b/bindings/cxx/examples/watch_line_value.cpp
new file mode 100644 (file)
index 0000000..ebc7fe7
--- /dev/null
@@ -0,0 +1,71 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+// SPDX-FileCopyrightText: 2023 Kent Gibson <warthog618@gmail.com>
+
+/* Minimal example of watching for edges on a single line. */
+
+#include <cstdlib>
+#include <filesystem>
+#include <gpiod.hpp>
+#include <iomanip>
+#include <iostream>
+
+namespace {
+
+/* Example configuration - customize to suit your situation. */
+const ::std::filesystem::path chip_path("/dev/gpiochip0");
+const ::gpiod::line::offset line_offset = 5;
+
+const char *edge_event_type_str(const ::gpiod::edge_event &event)
+{
+       switch (event.type()) {
+       case ::gpiod::edge_event::event_type::RISING_EDGE:
+               return "Rising";
+       case ::gpiod::edge_event::event_type::FALLING_EDGE:
+               return "Falling";
+       default:
+               return "Unknown";
+       }
+}
+
+} /* namespace */
+
+int main()
+{
+       /*
+        * Assume a button connecting the pin to ground, so pull it up and
+        * provide some debounce.
+        */
+       auto request =
+               ::gpiod::chip(chip_path)
+                       .prepare_request()
+                       .set_consumer("watch-line-value")
+                       .add_line_settings(
+                               line_offset,
+                               ::gpiod::line_settings()
+                                       .set_direction(
+                                               ::gpiod::line::direction::INPUT)
+                                       .set_edge_detection(
+                                               ::gpiod::line::edge::BOTH)
+                                       .set_bias(::gpiod::line::bias::PULL_UP)
+                                       .set_debounce_period(
+                                               std::chrono::milliseconds(10)))
+                       .do_request();
+
+       /*
+        * A larger buffer is an optimisation for reading bursts of events from
+        * the kernel, but that is not necessary in this case, so 1 is fine.
+        */
+       ::gpiod::edge_event_buffer buffer(1);
+
+       for (;;) {
+               /* Blocks until at least one event is available. */
+               request.read_edge_events(buffer);
+
+               for (const auto &event : buffer)
+                       ::std::cout << "line: " << event.line_offset()
+                                   << "  type: " << ::std::setw(7)
+                                   << ::std::left << edge_event_type_str(event)
+                                   << "  event #" << event.line_seqno()
+                                   << ::std::endl;
+       }
+}
diff --git a/bindings/cxx/examples/watch_multiple_line_values.cpp b/bindings/cxx/examples/watch_multiple_line_values.cpp
new file mode 100644 (file)
index 0000000..fb71fb2
--- /dev/null
@@ -0,0 +1,60 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+// SPDX-FileCopyrightText: 2023 Kent Gibson <warthog618@gmail.com>
+
+/* Minimal example of watching for edges on multiple lines. */
+
+#include <cstdlib>
+#include <gpiod.hpp>
+#include <iomanip>
+#include <iostream>
+
+namespace {
+
+/* Example configuration - customize to suit your situation */
+const ::std::filesystem::path chip_path("/dev/gpiochip0");
+const ::gpiod::line::offsets line_offsets = { 5, 3, 7 };
+
+const char *edge_event_type_str(const ::gpiod::edge_event &event)
+{
+       switch (event.type()) {
+       case ::gpiod::edge_event::event_type::RISING_EDGE:
+               return "Rising";
+       case ::gpiod::edge_event::event_type::FALLING_EDGE:
+               return "Falling";
+       default:
+               return "Unknown";
+       }
+}
+
+} /* namespace */
+
+int main()
+{
+       auto request =
+               ::gpiod::chip(chip_path)
+                       .prepare_request()
+                       .set_consumer("watch-multiple-line-values")
+                       .add_line_settings(
+                               line_offsets,
+                               ::gpiod::line_settings()
+                                       .set_direction(
+                                               ::gpiod::line::direction::INPUT)
+                                       .set_edge_detection(
+                                               ::gpiod::line::edge::BOTH))
+                       .do_request();
+
+       ::gpiod::edge_event_buffer buffer;
+
+       for (;;) {
+               /* Blocks until at leat one event available */
+               request.read_edge_events(buffer);
+
+               for (const auto &event : buffer)
+                       ::std::cout << "offset: " << event.line_offset()
+                                   << "  type: " << ::std::setw(7)
+                                   << ::std::left << edge_event_type_str(event)
+                                   << "  event #" << event.global_seqno()
+                                   << "  line event #" << event.line_seqno()
+                                   << ::std::endl;
+       }
+}
diff --git a/bindings/cxx/exception.cpp b/bindings/cxx/exception.cpp
new file mode 100644 (file)
index 0000000..378b249
--- /dev/null
@@ -0,0 +1,119 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// SPDX-FileCopyrightText: 2021-2022 Bartosz Golaszewski <brgl@bgdev.pl>
+
+#include "internal.hpp"
+
+namespace gpiod {
+
+GPIOD_CXX_API chip_closed::chip_closed(const ::std::string& what)
+       : ::std::logic_error(what)
+{
+
+}
+
+GPIOD_CXX_API chip_closed::chip_closed(const chip_closed& other) noexcept
+       : ::std::logic_error(other)
+{
+
+}
+
+GPIOD_CXX_API chip_closed::chip_closed(chip_closed&& other) noexcept
+       : ::std::logic_error(other)
+{
+
+}
+
+GPIOD_CXX_API chip_closed& chip_closed::operator=(const chip_closed& other) noexcept
+{
+       ::std::logic_error::operator=(other);
+
+       return *this;
+}
+
+GPIOD_CXX_API chip_closed& chip_closed::operator=(chip_closed&& other) noexcept
+{
+       ::std::logic_error::operator=(other);
+
+       return *this;
+}
+
+GPIOD_CXX_API chip_closed::~chip_closed()
+{
+
+}
+
+GPIOD_CXX_API request_released::request_released(const ::std::string& what)
+       : ::std::logic_error(what)
+{
+
+}
+
+GPIOD_CXX_API request_released::request_released(const request_released& other) noexcept
+       : ::std::logic_error(other)
+{
+
+}
+
+GPIOD_CXX_API request_released::request_released(request_released&& other) noexcept
+       : ::std::logic_error(other)
+{
+
+}
+
+GPIOD_CXX_API request_released& request_released::operator=(const request_released& other) noexcept
+{
+       ::std::logic_error::operator=(other);
+
+       return *this;
+}
+
+GPIOD_CXX_API request_released& request_released::operator=(request_released&& other) noexcept
+{
+       ::std::logic_error::operator=(other);
+
+       return *this;
+}
+
+GPIOD_CXX_API request_released::~request_released()
+{
+
+}
+
+GPIOD_CXX_API bad_mapping::bad_mapping(const ::std::string& what)
+       : ::std::runtime_error(what)
+{
+
+}
+
+GPIOD_CXX_API bad_mapping::bad_mapping(const bad_mapping& other) noexcept
+       : ::std::runtime_error(other)
+{
+
+}
+
+GPIOD_CXX_API bad_mapping::bad_mapping(bad_mapping&& other) noexcept
+       : ::std::runtime_error(other)
+{
+
+}
+
+GPIOD_CXX_API bad_mapping& bad_mapping::operator=(const bad_mapping& other) noexcept
+{
+       ::std::runtime_error::operator=(other);
+
+       return *this;
+}
+
+GPIOD_CXX_API bad_mapping& bad_mapping::operator=(bad_mapping&& other) noexcept
+{
+       ::std::runtime_error::operator=(other);
+
+       return *this;
+}
+
+GPIOD_CXX_API bad_mapping::~bad_mapping()
+{
+
+}
+
+} /* namespace gpiod */
diff --git a/bindings/cxx/gpiod.hpp b/bindings/cxx/gpiod.hpp
new file mode 100644 (file)
index 0000000..91e41a5
--- /dev/null
@@ -0,0 +1,48 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/* SPDX-FileCopyrightText: 2021-2022 Bartosz Golaszewski <brgl@bgdev.pl> */
+
+/**
+ * @file gpiod.hpp
+ */
+
+#ifndef __LIBGPIOD_GPIOD_CXX_HPP__
+#define __LIBGPIOD_GPIOD_CXX_HPP__
+
+/**
+ * @defgroup gpiod_cxx C++ bindings
+ *
+ * C++ bindings for libgpiod.
+ */
+
+/**
+ * @cond
+ */
+
+/*
+ * We don't make this symbol private because it needs to be accessible by
+ * the declarations in exception.hpp in order to expose the symbols of classes
+ * inheriting from standard exceptions.
+ */
+#define GPIOD_CXX_API __attribute__((visibility("default")))
+
+/**
+ * @endcond
+ */
+
+#define __LIBGPIOD_GPIOD_CXX_INSIDE__
+#include "gpiodcxx/chip.hpp"
+#include "gpiodcxx/chip-info.hpp"
+#include "gpiodcxx/edge-event.hpp"
+#include "gpiodcxx/edge-event-buffer.hpp"
+#include "gpiodcxx/exception.hpp"
+#include "gpiodcxx/info-event.hpp"
+#include "gpiodcxx/line.hpp"
+#include "gpiodcxx/line-config.hpp"
+#include "gpiodcxx/line-info.hpp"
+#include "gpiodcxx/line-request.hpp"
+#include "gpiodcxx/line-settings.hpp"
+#include "gpiodcxx/request-builder.hpp"
+#include "gpiodcxx/request-config.hpp"
+#undef __LIBGPIOD_GPIOD_CXX_INSIDE__
+
+#endif /* __LIBGPIOD_GPIOD_CXX_HPP__ */
diff --git a/bindings/cxx/gpiodcxx/Makefile.am b/bindings/cxx/gpiodcxx/Makefile.am
new file mode 100644 (file)
index 0000000..e3a3b9b
--- /dev/null
@@ -0,0 +1,20 @@
+# SPDX-License-Identifier: GPL-2.0-or-later
+# SPDX-FileCopyrightText: 2021 Bartosz Golaszewski <brgl@bgdev.pl>
+
+otherincludedir = $(includedir)/gpiodcxx
+otherinclude_HEADERS = \
+       chip.hpp \
+       chip-info.hpp \
+       edge-event-buffer.hpp \
+       edge-event.hpp \
+       exception.hpp \
+       info-event.hpp \
+       line.hpp \
+       line-config.hpp \
+       line-info.hpp \
+       line-request.hpp \
+       line-settings.hpp \
+       misc.hpp \
+       request-builder.hpp \
+       request-config.hpp \
+       timestamp.hpp
diff --git a/bindings/cxx/gpiodcxx/chip-info.hpp b/bindings/cxx/gpiodcxx/chip-info.hpp
new file mode 100644 (file)
index 0000000..e740e94
--- /dev/null
@@ -0,0 +1,105 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/* SPDX-FileCopyrightText: 2022 Bartosz Golaszewski <brgl@bgdev.pl> */
+
+/**
+ * @file chip-info.hpp
+ */
+
+#ifndef __LIBGPIOD_CXX_CHIP_INFO_HPP__
+#define __LIBGPIOD_CXX_CHIP_INFO_HPP__
+
+#if !defined(__LIBGPIOD_GPIOD_CXX_INSIDE__)
+#error "Only gpiod.hpp can be included directly."
+#endif
+
+#include <memory>
+#include <ostream>
+
+namespace gpiod {
+
+class chip;
+
+/**
+ * @ingroup gpiod_cxx
+ * @{
+ */
+
+/**
+ * @brief Represents an immutable snapshot of GPIO chip information.
+ */
+class chip_info final
+{
+public:
+
+       /**
+        * @brief Copy constructor.
+        * @param other Object to copy.
+        */
+       chip_info(const chip_info& other);
+
+       /**
+        * @brief Move constructor.
+        * @param other Object to move.
+        */
+       chip_info(chip_info&& other) noexcept;
+
+       ~chip_info();
+
+       /**
+        * @brief Assignment operator.
+        * @param other Object to copy.
+        * @return Reference to self.
+        */
+       chip_info& operator=(const chip_info& other);
+
+       /**
+        * @brief Move assignment operator.
+        * @param other Object to move.
+        * @return Reference to self.
+        */
+       chip_info& operator=(chip_info&& other) noexcept;
+
+       /**
+        * @brief Get the name of this GPIO chip.
+        * @return String containing the chip name.
+        */
+       ::std::string name() const noexcept;
+
+       /**
+        * @brief Get the label of this GPIO chip.
+        * @return String containing the chip name.
+        */
+       ::std::string label() const noexcept;
+
+       /**
+        * @brief Return the number of lines exposed by this chip.
+        * @return Number of lines.
+        */
+       ::std::size_t num_lines() const noexcept;
+
+private:
+
+       chip_info();
+
+       struct impl;
+
+       ::std::shared_ptr<impl> _m_priv;
+
+       friend chip;
+};
+
+/**
+ * @brief Stream insertion operator for GPIO chip objects.
+ * @param out Output stream to write to.
+ * @param chip GPIO chip to insert into the output stream.
+ * @return Reference to out.
+ */
+::std::ostream& operator<<(::std::ostream& out, const chip_info& chip);
+
+/**
+ * @}
+ */
+
+} /* namespace gpiod */
+
+#endif /* __LIBGPIOD_CXX_CHIP_INFO_HPP__ */
diff --git a/bindings/cxx/gpiodcxx/chip.hpp b/bindings/cxx/gpiodcxx/chip.hpp
new file mode 100644 (file)
index 0000000..39e0318
--- /dev/null
@@ -0,0 +1,182 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/* SPDX-FileCopyrightText: 2021-2022 Bartosz Golaszewski <brgl@bgdev.pl> */
+
+/**
+ * @file chip.hpp
+ */
+
+#ifndef __LIBGPIOD_CXX_CHIP_HPP__
+#define __LIBGPIOD_CXX_CHIP_HPP__
+
+#if !defined(__LIBGPIOD_GPIOD_CXX_INSIDE__)
+#error "Only gpiod.hpp can be included directly."
+#endif
+
+#include <chrono>
+#include <cstddef>
+#include <iostream>
+#include <filesystem>
+#include <memory>
+
+#include "line.hpp"
+
+namespace gpiod {
+
+class chip_info;
+class info_event;
+class line_config;
+class line_info;
+class line_request;
+class request_builder;
+class request_config;
+
+/**
+ * @ingroup gpiod_cxx
+ * @{
+ */
+
+/**
+ * @brief Represents a GPIO chip.
+ */
+class chip final
+{
+public:
+
+       /**
+        * @brief Instantiates a new chip object by opening the device file
+        *        indicated by the \p path argument.
+        * @param path Path to the device file to open.
+        */
+       explicit chip(const ::std::filesystem::path& path);
+
+       /**
+        * @brief Move constructor.
+        * @param other Object to move.
+        */
+       chip(chip&& other) noexcept;
+
+       ~chip();
+
+       chip& operator=(const chip& other) = delete;
+
+       /**
+        * @brief Move assignment operator.
+        * @param other Object to move.
+        * @return Reference to self.
+        */
+       chip& operator=(chip&& other) noexcept;
+
+       /**
+        * @brief Check if this object is valid.
+        * @return True if this object's methods can be used, false otherwise.
+        *         False usually means the chip was closed. If the user calls
+        *         any of the methods of this class on an object for which this
+        *         operator returned false, a logic_error will be thrown.
+        */
+       explicit operator bool() const noexcept;
+
+       /**
+        * @brief Close the GPIO chip device file and free associated resources.
+        * @note The chip object can live after calling this method but any of
+        *       the chip's mutators will throw a logic_error exception.
+        */
+       void close();
+
+       /**
+        * @brief Get the filesystem path that was used to open this GPIO chip.
+        * @return Path to the underlying character device file.
+        */
+       ::std::filesystem::path path() const;
+
+       /**
+        * @brief Get information about the chip.
+        * @return New chip_info object.
+        */
+       chip_info get_info() const;
+
+       /**
+        * @brief Retrieve the current snapshot of line information for a
+        *        single line.
+        * @param offset Offset of the line to get the info for.
+        * @return New ::gpiod::line_info object.
+        */
+       line_info get_line_info(line::offset offset) const;
+
+       /**
+        * @brief Wrapper around ::gpiod::chip::get_line_info that retrieves
+        *        the line info and starts watching the line for changes.
+        * @param offset Offset of the line to get the info for.
+        * @return New ::gpiod::line_info object.
+        */
+       line_info watch_line_info(line::offset offset) const;
+
+       /**
+        * @brief Stop watching the line at given offset for info events.
+        * @param offset Offset of the line to get the info for.
+        */
+       void unwatch_line_info(line::offset offset) const;
+
+       /**
+        * @brief Get the file descriptor associated with this chip.
+        * @return File descriptor number.
+        */
+       int fd() const;
+
+       /**
+        * @brief Wait for line status events on any of the watched lines
+        *        exposed by this chip.
+        * @param timeout Wait time limit in nanoseconds. If set to 0, the
+        *                function returns immediatelly. If set to a negative
+        *                number, the function blocks indefinitely until an
+        *                event becomes available.
+        * @return True if at least one event is ready to be read. False if the
+        *         wait timed out.
+        */
+       bool wait_info_event(const ::std::chrono::nanoseconds& timeout) const;
+
+       /**
+        * @brief Read a single line status change event from this chip.
+        * @return New info_event object.
+        */
+       info_event read_info_event() const;
+
+       /**
+        * @brief Map a GPIO line's name to its offset within the chip.
+        * @param name Name of the GPIO line to map.
+        * @return Offset of the line within the chip or -1 if the line with
+        *         given name is not exposed by this chip.
+        */
+       int get_line_offset_from_name(const ::std::string& name) const;
+
+       /**
+        * @brief Create a request_builder associated with this chip.
+        * @return New request_builder object.
+        */
+       request_builder prepare_request();
+
+private:
+
+       struct impl;
+
+       ::std::shared_ptr<impl> _m_priv;
+
+       chip(const chip& other);
+
+       friend request_builder;
+};
+
+/**
+ * @brief Stream insertion operator for GPIO chip objects.
+ * @param out Output stream to write to.
+ * @param chip GPIO chip to insert into the output stream.
+ * @return Reference to out.
+ */
+::std::ostream& operator<<(::std::ostream& out, const chip& chip);
+
+/**
+ * @}
+ */
+
+} /* namespace gpiod */
+
+#endif /* __LIBGPIOD_CXX_CHIP_HPP__ */
diff --git a/bindings/cxx/gpiodcxx/edge-event-buffer.hpp b/bindings/cxx/gpiodcxx/edge-event-buffer.hpp
new file mode 100644 (file)
index 0000000..2482e8a
--- /dev/null
@@ -0,0 +1,129 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/* SPDX-FileCopyrightText: 2021-2022 Bartosz Golaszewski <brgl@bgdev.pl> */
+
+/**
+ * @file edge-event-buffer.hpp
+ */
+
+#ifndef __LIBGPIOD_CXX_EDGE_EVENT_BUFFER_HPP__
+#define __LIBGPIOD_CXX_EDGE_EVENT_BUFFER_HPP__
+
+#if !defined(__LIBGPIOD_GPIOD_CXX_INSIDE__)
+#error "Only gpiod.hpp can be included directly."
+#endif
+
+#include <cstddef>
+#include <iostream>
+#include <memory>
+#include <vector>
+
+namespace gpiod {
+
+class edge_event;
+class line_request;
+
+/**
+ * @ingroup gpiod_cxx
+ * @{
+ */
+
+/**
+ * @brief Object into which edge events are read for better performance.
+ *
+ * The edge_event_buffer allows reading edge_event objects into an existing
+ * buffer which improves the performance by avoiding needless memory
+ * allocations.
+ */
+class edge_event_buffer final
+{
+public:
+
+       /**
+        * @brief Constant iterator for iterating over edge events stored in
+        *        the buffer.
+        */
+       using const_iterator = ::std::vector<edge_event>::const_iterator;
+
+       /**
+        * @brief Constructor. Creates a new edge event buffer with given
+        *        capacity.
+        * @param capacity Capacity of the new buffer.
+        */
+       explicit edge_event_buffer(::std::size_t capacity = 64);
+
+       edge_event_buffer(const edge_event_buffer& other) = delete;
+
+       /**
+        * @brief Move constructor.
+        * @param other Object to move.
+        */
+       edge_event_buffer(edge_event_buffer&& other) noexcept;
+
+       ~edge_event_buffer();
+
+       edge_event_buffer& operator=(const edge_event_buffer& other) = delete;
+
+       /**
+        * @brief Move assignment operator.
+        * @param other Object to move.
+        * @return Reference to self.
+        */
+       edge_event_buffer& operator=(edge_event_buffer&& other) noexcept;
+
+       /**
+        * @brief Get the constant reference to the edge event at given index.
+        * @param index Index of the event in the buffer.
+        * @return Constant reference to the edge event.
+        */
+       const edge_event& get_event(unsigned int index) const;
+
+       /**
+        * @brief Get the number of edge events currently stored in the buffer.
+        * @return Number of edge events in the buffer.
+        */
+       ::std::size_t num_events() const;
+
+       /**
+        * @brief Maximum capacity of the buffer.
+        * @return Buffer capacity.
+        */
+       ::std::size_t capacity() const noexcept;
+
+       /**
+        * @brief Get a constant iterator to the first edge event currently
+        *        stored in the buffer.
+        * @return Constant iterator to the first element.
+        */
+       const_iterator begin() const noexcept;
+
+       /**
+        * @brief Get a constant iterator to the element after the last edge
+        *        event in the buffer.
+        * @return Constant iterator to the element after the last edge event.
+        */
+       const_iterator end() const noexcept;
+
+private:
+
+       struct impl;
+
+       ::std::unique_ptr<impl> _m_priv;
+
+       friend line_request;
+};
+
+/**
+ * @brief Stream insertion operator for GPIO edge event buffer objects.
+ * @param out Output stream to write to.
+ * @param buf GPIO edge event buffer object to insert into the output stream.
+ * @return Reference to out.
+ */
+::std::ostream& operator<<(::std::ostream& out, const edge_event_buffer& buf);
+
+/**
+ * @}
+ */
+
+} /* namespace gpiod */
+
+#endif /* __LIBGPIOD_CXX_EDGE_EVENT_BUFFER_HPP__ */
diff --git a/bindings/cxx/gpiodcxx/edge-event.hpp b/bindings/cxx/gpiodcxx/edge-event.hpp
new file mode 100644 (file)
index 0000000..47c256a
--- /dev/null
@@ -0,0 +1,137 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/* SPDX-FileCopyrightText: 2021-2022 Bartosz Golaszewski <brgl@bgdev.pl> */
+
+/**
+ * @file edge-event.hpp
+ */
+
+#ifndef __LIBGPIOD_CXX_EDGE_EVENT_HPP__
+#define __LIBGPIOD_CXX_EDGE_EVENT_HPP__
+
+#if !defined(__LIBGPIOD_GPIOD_CXX_INSIDE__)
+#error "Only gpiod.hpp can be included directly."
+#endif
+
+#include <cstdint>
+#include <iostream>
+#include <memory>
+
+#include "timestamp.hpp"
+
+namespace gpiod {
+
+class edge_event_buffer;
+
+/**
+ * @ingroup gpiod_cxx
+ * @{
+ */
+
+/**
+ * @brief Immutable object containing data about a single edge event.
+ */
+class edge_event final
+{
+public:
+
+       /**
+        * @brief Edge event types.
+        */
+       enum class event_type
+       {
+               RISING_EDGE = 1,
+               /**< This is a rising edge event. */
+               FALLING_EDGE,
+               /**< This is falling edge event. */
+       };
+
+       /**
+        * @brief Copy constructor.
+        * @param other Object to copy.
+        */
+       edge_event(const edge_event& other);
+
+       /**
+        * @brief Move constructor.
+        * @param other Object to move.
+        */
+       edge_event(edge_event&& other) noexcept;
+
+       ~edge_event();
+
+       /**
+        * @brief Copy assignment operator.
+        * @param other Object to copy.
+        * @return Reference to self.
+        */
+       edge_event& operator=(const edge_event& other);
+
+       /**
+        * @brief Move assignment operator.
+        * @param other Object to move.
+        * @return Reference to self.
+        */
+       edge_event& operator=(edge_event&& other) noexcept;
+
+       /**
+        * @brief Retrieve the event type.
+        * @return Event type (rising or falling edge).
+        */
+       event_type type() const;
+
+       /**
+        * @brief Retrieve the event time-stamp.
+        * @return Time-stamp in nanoseconds as registered by the kernel using
+        *         the configured edge event clock.
+        */
+       timestamp timestamp_ns() const noexcept;
+
+       /**
+        * @brief Read the offset of the line on which this event was
+        *        registered.
+        * @return Line offset.
+        */
+       line::offset line_offset() const noexcept;
+
+       /**
+        * @brief Get the global sequence number of this event.
+        * @return Sequence number of the event relative to all lines in the
+        *         associated line request.
+        */
+       unsigned long global_seqno() const noexcept;
+
+       /**
+        * @brief Get the event sequence number specific to the concerned line.
+        * @return Sequence number of the event relative to this line within
+        *         the lifetime of the associated line request.
+        */
+       unsigned long line_seqno() const noexcept;
+
+private:
+
+       edge_event();
+
+       struct impl;
+       struct impl_managed;
+       struct impl_external;
+
+       ::std::shared_ptr<impl> _m_priv;
+
+       friend edge_event_buffer;
+};
+
+/**
+ * @brief Stream insertion operator for edge events.
+ * @param out Output stream to write to.
+ * @param event Edge event to insert into the output stream.
+ * @return Reference to out.
+ */
+::std::ostream& operator<<(::std::ostream& out, const edge_event& event);
+
+/**
+ * @}
+ */
+
+} /* namespace gpiod */
+
+#endif /* __LIBGPIOD_CXX_EDGE_EVENT_HPP__ */
diff --git a/bindings/cxx/gpiodcxx/exception.hpp b/bindings/cxx/gpiodcxx/exception.hpp
new file mode 100644 (file)
index 0000000..34737d2
--- /dev/null
@@ -0,0 +1,158 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/* SPDX-FileCopyrightText: 2021-2022 Bartosz Golaszewski <brgl@bgdev.pl> */
+
+/**
+ * @file exception.hpp
+ */
+
+#ifndef __LIBGPIOD_CXX_EXCEPTION_HPP__
+#define __LIBGPIOD_CXX_EXCEPTION_HPP__
+
+#if !defined(__LIBGPIOD_GPIOD_CXX_INSIDE__)
+#error "Only gpiod.hpp can be included directly."
+#endif
+
+#include <stdexcept>
+#include <string>
+
+namespace gpiod {
+
+/**
+ * @ingroup gpiod_cxx
+ * @{
+ */
+
+/**
+ * @brief Exception thrown when an already closed chip is used.
+ */
+class GPIOD_CXX_API chip_closed final : public ::std::logic_error
+{
+public:
+
+       /**
+        * @brief Constructor.
+        * @param what Human readable reason for error.
+        */
+       explicit chip_closed(const ::std::string& what);
+
+       /**
+        * @brief Copy constructor.
+        * @param other Object to copy from.
+        */
+       chip_closed(const chip_closed& other) noexcept;
+
+       /**
+        * @brief Move constructor.
+        * @param other Object to move.
+        */
+       chip_closed(chip_closed&& other) noexcept;
+
+       /**
+        * @brief Assignment operator.
+        * @param other Object to copy from.
+        * @return Reference to self.
+        */
+       chip_closed& operator=(const chip_closed& other) noexcept;
+
+       /**
+        * @brief Move assignment operator.
+        * @param other Object to move.
+        * @return Reference to self.
+        */
+       chip_closed& operator=(chip_closed&& other) noexcept;
+
+       ~chip_closed();
+};
+
+/**
+ * @brief Exception thrown when an already released line request is used.
+ */
+class GPIOD_CXX_API request_released final : public ::std::logic_error
+{
+public:
+
+       /**
+        * @brief Constructor.
+        * @param what Human readable reason for error.
+        */
+       explicit request_released(const ::std::string& what);
+
+       /**
+        * @brief Copy constructor.
+        * @param other Object to copy from.
+        */
+       request_released(const request_released& other) noexcept;
+
+       /**
+        * @brief Move constructor.
+        * @param other Object to move.
+        */
+       request_released(request_released&& other) noexcept;
+
+       /**
+        * @brief Assignment operator.
+        * @param other Object to copy from.
+        * @return Reference to self.
+        */
+       request_released& operator=(const request_released& other) noexcept;
+
+       /**
+        * @brief Move assignment operator.
+        * @param other Object to move.
+        * @return Reference to self.
+        */
+       request_released& operator=(request_released&& other) noexcept;
+
+       ~request_released();
+};
+
+/**
+ * @brief Exception thrown when the core C library returns an invalid value
+ *        for any of the line_info properties.
+ */
+class GPIOD_CXX_API bad_mapping final : public ::std::runtime_error
+{
+public:
+
+       /**
+        * @brief Constructor.
+        * @param what Human readable reason for error.
+        */
+       explicit bad_mapping(const ::std::string& what);
+
+       /**
+        * @brief Copy constructor.
+        * @param other Object to copy from.
+        */
+       bad_mapping(const bad_mapping& other) noexcept;
+
+       /**
+        * @brief Move constructor.
+        * @param other Object to move.
+        */
+       bad_mapping(bad_mapping&& other) noexcept;
+
+       /**
+        * @brief Assignment operator.
+        * @param other Object to copy from.
+        * @return Reference to self.
+        */
+       bad_mapping& operator=(const bad_mapping& other) noexcept;
+
+       /**
+        * @brief Move assignment operator.
+        * @param other Object to move.
+        * @return Reference to self.
+        */
+       bad_mapping& operator=(bad_mapping&& other) noexcept;
+
+       ~bad_mapping();
+};
+
+/**
+ * @}
+ */
+
+} /* namespace gpiod */
+
+#endif /* __LIBGPIOD_CXX_EXCEPTION_HPP__ */
diff --git a/bindings/cxx/gpiodcxx/info-event.hpp b/bindings/cxx/gpiodcxx/info-event.hpp
new file mode 100644 (file)
index 0000000..69b88b6
--- /dev/null
@@ -0,0 +1,123 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/* SPDX-FileCopyrightText: 2021-2022 Bartosz Golaszewski <brgl@bgdev.pl> */
+
+/**
+ * @file gpiod.h
+ */
+
+#ifndef __LIBGPIOD_CXX_INFO_EVENT_HPP__
+#define __LIBGPIOD_CXX_INFO_EVENT_HPP__
+
+#if !defined(__LIBGPIOD_GPIOD_CXX_INSIDE__)
+#error "Only gpiod.hpp can be included directly."
+#endif
+
+#include <cstdint>
+#include <iostream>
+#include <memory>
+
+#include "timestamp.hpp"
+
+namespace gpiod {
+
+class chip;
+class line_info;
+
+/**
+ * @ingroup gpiod_cxx
+ * @{
+ */
+
+/**
+ * @brief Immutable object containing data about a single line info event.
+ */
+class info_event final
+{
+public:
+
+       /**
+        * @brief Types of info events.
+        */
+       enum class event_type
+       {
+               LINE_REQUESTED = 1,
+               /**< Line has been requested. */
+               LINE_RELEASED,
+               /**< Previously requested line has been released. */
+               LINE_CONFIG_CHANGED,
+               /**< Line configuration has changed. */
+       };
+
+       /**
+        * @brief Copy constructor.
+        * @param other Object to copy.
+        */
+       info_event(const info_event& other);
+
+       /**
+        * @brief Move constructor.
+        * @param other Object to move.
+        */
+       info_event(info_event&& other) noexcept;
+
+       ~info_event();
+
+       /**
+        * @brief Copy assignment operator.
+        * @param other Object to copy.
+        * @return Reference to self.
+        */
+       info_event& operator=(const info_event& other);
+
+       /**
+        * @brief Move assignment operator.
+        * @param other Object to move.
+        * @return Reference to self.
+        */
+       info_event& operator=(info_event&& other) noexcept;
+
+       /**
+        * @brief Type of this event.
+        * @return Event type.
+        */
+       event_type type() const;
+
+       /**
+        * @brief Timestamp of the event as returned by the kernel.
+        * @return Timestamp as a 64-bit unsigned integer.
+        */
+       ::std::uint64_t timestamp_ns() const noexcept;
+
+       /**
+        * @brief Get the new line information.
+        * @return Constant reference to the line info object containing the
+        *         line data as read at the time of the info event.
+        */
+       const line_info& get_line_info() const noexcept;
+
+private:
+
+       info_event();
+
+       struct impl;
+
+       ::std::shared_ptr<impl> _m_priv;
+
+       friend chip;
+};
+
+/**
+ * @brief Stream insertion operator for info events.
+ * @param out Output stream to write to.
+ * @param event GPIO line info event to insert into the output stream.
+ * @return Reference to out.
+ */
+::std::ostream& operator<<(::std::ostream& out, const info_event& event);
+
+/**
+ * @}
+ */
+
+} /* namespace gpiod */
+
+#endif /* __LIBGPIOD_CXX_INFO_EVENT_HPP__ */
diff --git a/bindings/cxx/gpiodcxx/line-config.hpp b/bindings/cxx/gpiodcxx/line-config.hpp
new file mode 100644 (file)
index 0000000..58c9d87
--- /dev/null
@@ -0,0 +1,120 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/* SPDX-FileCopyrightText: 2021-2022 Bartosz Golaszewski <brgl@bgdev.pl> */
+
+/**
+ * @file line-config.hpp
+ */
+
+#ifndef __LIBGPIOD_CXX_LINE_CONFIG_HPP__
+#define __LIBGPIOD_CXX_LINE_CONFIG_HPP__
+
+#if !defined(__LIBGPIOD_GPIOD_CXX_INSIDE__)
+#error "Only gpiod.hpp can be included directly."
+#endif
+
+#include <map>
+#include <memory>
+
+namespace gpiod {
+
+class chip;
+class line_request;
+class line_settings;
+
+/**
+ * @ingroup gpiod_cxx
+ * @{
+ */
+
+/**
+ * @brief Contains a set of line config options used in line requests and
+ *        reconfiguration.
+ */
+class line_config final
+{
+public:
+
+
+       line_config();
+
+       line_config(const line_config& other) = delete;
+
+       /**
+        * @brief Move constructor.
+        * @param other Object to move.
+        */
+       line_config(line_config&& other) noexcept;
+
+       ~line_config();
+
+       /**
+        * @brief Move assignment operator.
+        * @param other Object to move.
+        * @return Reference to self.
+        */
+       line_config& operator=(line_config&& other) noexcept;
+
+       /**
+        * @brief Reset the line config object.
+        * @return Reference to self.
+        */
+       line_config& reset() noexcept;
+
+       /**
+        * @brief Add line settings for a single offset.
+        * @param offset Offset for which to add settings.
+        * @param settings Line settings to add.
+        * @return Reference to self.
+        */
+       line_config& add_line_settings(line::offset offset, const line_settings& settings);
+
+       /**
+        * @brief Add line settings for a set of offsets.
+        * @param offsets Offsets for which to add settings.
+        * @param settings Line settings to add.
+        * @return Reference to self.
+        */
+       line_config& add_line_settings(const line::offsets& offsets, const line_settings& settings);
+
+       /**
+        * @brief Set output values for a number of lines.
+        * @param values Buffer containing the output values.
+        * @return Reference to self.
+        */
+       line_config& set_output_values(const line::values& values);
+
+       /**
+        * @brief Get a mapping of offsets to line settings stored by this
+        *        object.
+        * @return Map in which keys represent line offsets and values are
+        *         the settings corresponding with them.
+        */
+       ::std::map<line::offset, line_settings> get_line_settings() const;
+
+private:
+
+       struct impl;
+
+       ::std::shared_ptr<impl> _m_priv;
+
+       line_config& operator=(const line_config& other);
+
+       friend line_request;
+       friend request_builder;
+};
+
+/**
+ * @brief Stream insertion operator for GPIO line config objects.
+ * @param out Output stream to write to.
+ * @param config Line config object to insert into the output stream.
+ * @return Reference to out.
+ */
+::std::ostream& operator<<(::std::ostream& out, const line_config& config);
+
+/**
+ * @}
+ */
+
+} /* namespace gpiod */
+
+#endif /* __LIBGPIOD_CXX_LINE_CONFIG_HPP__ */
diff --git a/bindings/cxx/gpiodcxx/line-info.hpp b/bindings/cxx/gpiodcxx/line-info.hpp
new file mode 100644 (file)
index 0000000..8b10dc4
--- /dev/null
@@ -0,0 +1,176 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/* SPDX-FileCopyrightText: 2021-2022 Bartosz Golaszewski <brgl@bgdev.pl> */
+
+/**
+ * @file line-info.hpp
+ */
+
+#ifndef __LIBGPIOD_CXX_LINE_INFO_HPP__
+#define __LIBGPIOD_CXX_LINE_INFO_HPP__
+
+#if !defined(__LIBGPIOD_GPIOD_CXX_INSIDE__)
+#error "Only gpiod.hpp can be included directly."
+#endif
+
+#include <chrono>
+#include <iostream>
+#include <memory>
+#include <string>
+
+namespace gpiod {
+
+class chip;
+class info_event;
+
+/**
+ * @ingroup gpiod_cxx
+ * @{
+ */
+
+/**
+ * @brief Contains an immutable snapshot of the line's state at the
+ *        time when the object of this class was instantiated.
+ */
+class line_info final
+{
+public:
+
+       /**
+        * @brief Copy constructor.
+        * @param other Object to copy.
+        */
+       line_info(const line_info& other) noexcept;
+
+       /**
+        * @brief Move constructor.
+        * @param other Object to move.
+        */
+       line_info(line_info&& other) noexcept;
+
+       ~line_info();
+
+       /**
+        * @brief Copy assignment operator.
+        * @param other Object to copy.
+        * @return Reference to self.
+        */
+       line_info& operator=(const line_info& other) noexcept;
+
+       /**
+        * @brief Move assignment operator.
+        * @param other Object to move.
+        * @return Reference to self.
+        */
+       line_info& operator=(line_info&& other) noexcept;
+
+       /**
+        * @brief Get the hardware offset of the line.
+        * @return Offset of the line within the parent chip.
+        */
+       line::offset offset() const noexcept;
+
+       /**
+        * @brief Get the GPIO line name.
+        * @return Name of the GPIO line as it is represented in the kernel.
+        *         This routine returns an empty string if the line is unnamed.
+        */
+       ::std::string name() const noexcept;
+
+       /**
+        * @brief Check if the line is currently in use.
+        * @return True if the line is in use, false otherwise.
+        *
+        * The user space can't know exactly why a line is busy. It may have
+        * been requested by another process or hogged by the kernel. It only
+        * matters that the line is used and we can't request it.
+        */
+       bool used() const noexcept;
+
+       /**
+        * @brief Read the GPIO line consumer name.
+        * @return Name of the GPIO consumer name as it is represented in the
+        *         kernel. This routine returns an empty string if the line is
+        *         not used.
+        */
+       ::std::string consumer() const noexcept;
+
+       /**
+        * @brief Read the GPIO line direction setting.
+        * @return Returns DIRECTION_INPUT or DIRECTION_OUTPUT.
+        */
+       line::direction direction() const;
+
+       /**
+        * @brief Read the current edge detection setting of this line.
+        * @return Returns EDGE_NONE, EDGE_RISING, EDGE_FALLING or EDGE_BOTH.
+        */
+       line::edge edge_detection() const;
+
+       /**
+        * @brief Read the GPIO line bias setting.
+        * @return Returns BIAS_PULL_UP, BIAS_PULL_DOWN, BIAS_DISABLE or
+        *         BIAS_UNKNOWN.
+        */
+       line::bias bias() const;
+
+       /**
+        * @brief Read the GPIO line drive setting.
+        * @return Returns DRIVE_PUSH_PULL, DRIVE_OPEN_DRAIN or
+        *         DRIVE_OPEN_SOURCE.
+        */
+       line::drive drive() const;
+
+       /**
+        * @brief Check if the signal of this line is inverted.
+        * @return True if this line is "active-low", false otherwise.
+        */
+       bool active_low() const noexcept;
+
+       /**
+        * @brief Check if this line is debounced (either by hardware or by the
+        *        kernel software debouncer).
+        * @return True if the line is debounced, false otherwise.
+        */
+       bool debounced() const noexcept;
+
+       /**
+        * @brief Read the current debounce period in microseconds.
+        * @return Current debounce period in microseconds, 0 if the line is
+        *         not debounced.
+        */
+       ::std::chrono::microseconds debounce_period() const noexcept;
+
+       /**
+        * @brief Read the current event clock setting used for edge event
+        *        timestamps.
+        * @return Returns MONOTONIC, REALTIME or HTE.
+        */
+       line::clock event_clock() const;
+
+private:
+
+       line_info();
+
+       struct impl;
+
+       ::std::shared_ptr<impl> _m_priv;
+
+       friend chip;
+       friend info_event;
+};
+
+/**
+ * @brief Stream insertion operator for GPIO line info objects.
+ * @param out Output stream to write to.
+ * @param info GPIO line info object to insert into the output stream.
+ * @return Reference to out.
+ */
+::std::ostream& operator<<(::std::ostream& out, const line_info& info);
+
+/**
+ * @}
+ */
+
+} /* namespace gpiod */
+
+#endif /* __LIBGPIOD_CXX_LINE_INFO_HPP__ */
diff --git a/bindings/cxx/gpiodcxx/line-request.hpp b/bindings/cxx/gpiodcxx/line-request.hpp
new file mode 100644 (file)
index 0000000..a658825
--- /dev/null
@@ -0,0 +1,236 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/* SPDX-FileCopyrightText: 2021-2022 Bartosz Golaszewski <brgl@bgdev.pl> */
+
+/**
+ * @file line-request.hpp
+ */
+
+#ifndef __LIBGPIOD_CXX_LINE_REQUEST_HPP__
+#define __LIBGPIOD_CXX_LINE_REQUEST_HPP__
+
+#if !defined(__LIBGPIOD_GPIOD_CXX_INSIDE__)
+#error "Only gpiod.hpp can be included directly."
+#endif
+
+#include <chrono>
+#include <cstddef>
+#include <iostream>
+#include <memory>
+
+#include "misc.hpp"
+
+namespace gpiod {
+
+class chip;
+class edge_event;
+class edge_event_buffer;
+class line_config;
+
+/**
+ * @ingroup gpiod_cxx
+ * @{
+ */
+
+/**
+ * @brief Stores the context of a set of requested GPIO lines.
+ */
+class line_request final
+{
+public:
+
+       line_request(const line_request& other) = delete;
+
+       /**
+        * @brief Move constructor.
+        * @param other Object to move.
+        */
+       line_request(line_request&& other) noexcept;
+
+       ~line_request();
+
+       line_request& operator=(const line_request& other) = delete;
+
+       /**
+        * @brief Move assignment operator.
+        * @param other Object to move.
+        * @return Reference to self.
+        */
+       line_request& operator=(line_request&& other) noexcept;
+
+       /**
+        * @brief Check if this object is valid.
+        * @return True if this object's methods can be used, false otherwise.
+        *         False usually means the request was released. If the user
+        *         calls any of the methods of this class on an object for
+        *         which this operator returned false, a logic_error will be
+        *         thrown.
+        */
+       explicit operator bool() const noexcept;
+
+       /**
+        * @brief Release the GPIO chip and free all associated resources.
+        * @note The object can still be used after this method is called but
+        *       using any of the mutators will result in throwing
+        *       a logic_error exception.
+        */
+       void release();
+
+       /**
+        * @brief Get the name of the chip this request was made on.
+        * @return Name to the GPIO chip.
+        */
+       ::std::string chip_name() const;
+
+       /**
+        * @brief Get the number of requested lines.
+        * @return Number of lines in this request.
+        */
+       ::std::size_t num_lines() const;
+
+       /**
+        * @brief Get the list of offsets of requested lines.
+        * @return List of hardware offsets of the lines in this request.
+        */
+       line::offsets offsets() const;
+
+       /**
+        * @brief Get the value of a single requested line.
+        * @param offset Offset of the line to read within the chip.
+        * @return Current line value.
+        */
+       line::value get_value(line::offset offset);
+
+       /**
+        * @brief Get the values of a subset of requested lines.
+        * @param offsets Vector of line offsets
+        * @return Vector of lines values with indexes of values corresponding
+        *         to those of the offsets.
+        */
+       line::values get_values(const line::offsets& offsets);
+
+       /**
+        * @brief Get the values of all requested lines.
+        * @return List of read values.
+        */
+       line::values get_values();
+
+       /**
+        * @brief Get the values of a subset of requested lines into a vector
+        *        supplied by the caller.
+        * @param offsets Vector of line offsets.
+        * @param values Vector for storing the values. Its size must be at
+        *               least that of the offsets vector. The indexes of read
+        *               values will correspond with those in the offsets
+        *               vector.
+        */
+       void get_values(const line::offsets& offsets, line::values& values);
+
+       /**
+        * @brief Get the values of all requested lines.
+        * @param values Array in which the values will be stored. Must hold
+        *               at least the number of elements returned by
+        *               line_request::num_lines.
+        */
+       void get_values(line::values& values);
+
+       /**
+        * @brief Set the value of a single requested line.
+        * @param offset Offset of the line to set within the chip.
+        * @param value New line value.
+        * @return Reference to self.
+        */
+       line_request& set_value(line::offset offset, line::value value);
+
+       /**
+        * @brief Set the values of a subset of requested lines.
+        * @param values Vector containing a set of offset->value mappings.
+        * @return Reference to self.
+        */
+       line_request& set_values(const line::value_mappings& values);
+
+       /**
+        * @brief Set the values of a subset of requested lines.
+        * @param offsets Vector containing the offsets of lines to set.
+        * @param values Vector containing new values with indexes
+        *               corresponding with those in the offsets vector.
+        * @return Reference to self.
+        */
+       line_request& set_values(const line::offsets& offsets, const line::values& values);
+
+       /**
+        * @brief Set the values of all requested lines.
+        * @param values Array of new line values. The size must be equal to
+        *               the value returned by line_request::num_lines.
+        * @return Reference to self.
+        */
+       line_request& set_values(const line::values& values);
+
+       /**
+        * @brief Apply new config options to requested lines.
+        * @param config New configuration.
+        * @return Reference to self.
+        */
+       line_request& reconfigure_lines(const line_config& config);
+
+       /**
+        * @brief Get the file descriptor number associated with this line
+        *        request.
+        * @return File descriptor number.
+        */
+       int fd() const;
+
+       /**
+        * @brief Wait for edge events on any of the lines requested with edge
+        *        detection enabled.
+        * @param timeout Wait time limit in nanoseconds. If set to 0, the
+        *                function returns immediatelly. If set to a negative
+        *                number, the function blocks indefinitely until an
+        *                event becomes available.
+        * @return True if at least one event is ready to be read. False if the
+        *         wait timed out.
+        */
+       bool wait_edge_events(const ::std::chrono::nanoseconds& timeout) const;
+
+       /**
+        * @brief Read a number of edge events from this request up to the
+        *        maximum capacity of the buffer.
+        * @param buffer Edge event buffer to read events into.
+        * @return Number of events read.
+        */
+       ::std::size_t read_edge_events(edge_event_buffer& buffer);
+
+       /**
+        * @brief Read a number of edge events from this request.
+        * @param buffer Edge event buffer to read events into.
+        * @param max_events Maximum number of events to read. Limited by the
+        *                   capacity of the buffer.
+        * @return Number of events read.
+        */
+       ::std::size_t read_edge_events(edge_event_buffer& buffer, ::std::size_t max_events);
+
+private:
+
+       line_request();
+
+       struct impl;
+
+       ::std::unique_ptr<impl> _m_priv;
+
+       friend request_builder;
+};
+
+/**
+ * @brief Stream insertion operator for line requests.
+ * @param out Output stream to write to.
+ * @param request Line request object to insert into the output stream.
+ * @return Reference to out.
+ */
+::std::ostream& operator<<(::std::ostream& out, const line_request& request);
+
+/**
+ * @}
+ */
+
+} /* namespace gpiod */
+
+#endif /* __LIBGPIOD_CXX_LINE_REQUEST_HPP__ */
diff --git a/bindings/cxx/gpiodcxx/line-settings.hpp b/bindings/cxx/gpiodcxx/line-settings.hpp
new file mode 100644 (file)
index 0000000..1004ccd
--- /dev/null
@@ -0,0 +1,202 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/* SPDX-FileCopyrightText: 2022 Bartosz Golaszewski <brgl@bgdev.pl> */
+
+/**
+ * @file request-config.hpp
+ */
+
+#ifndef __LIBGPIOD_CXX_LINE_SETTINGS_HPP__
+#define __LIBGPIOD_CXX_LINE_SETTINGS_HPP__
+
+#if !defined(__LIBGPIOD_GPIOD_CXX_INSIDE__)
+#error "Only gpiod.hpp can be included directly."
+#endif
+
+#include <chrono>
+#include <memory>
+
+#include "line.hpp"
+
+namespace gpiod {
+
+class line_config;
+
+/**
+ * @ingroup gpiod_cxx
+ * @{
+ */
+
+/**
+ * @brief Stores GPIO line settings.
+ */
+class line_settings final
+{
+public:
+
+       /**
+        * @brief Initializes the line_settings object with default values.
+        */
+       line_settings();
+
+       /**
+        * @brief Copy constructor.
+        * @param other Object to copy.
+        */
+       line_settings(const line_settings& other);
+
+       /**
+        * @brief Move constructor.
+        * @param other Object to move.
+        */
+       line_settings(line_settings&& other) noexcept;
+
+       ~line_settings();
+
+       /**
+        * @brief Copy assignment operator.
+        * @param other Object to copy.
+        * @return Reference to self.
+        */
+       line_settings& operator=(const line_settings& other);
+
+       /**
+        * @brief Move assignment operator.
+        * @param other Object to move.
+        * @return Reference to self.
+        */
+       line_settings& operator=(line_settings&& other);
+
+       /**
+        * @brief Reset the line settings to default values.
+        * @return Reference to self.
+        */
+       line_settings& reset() noexcept;
+
+       /**
+        * @brief Set direction.
+        * @param direction New direction.
+        * @return Reference to self.
+        */
+       line_settings& set_direction(line::direction direction);
+
+       /**
+        * @brief Get direction.
+        * @return Current direction setting.
+        */
+       line::direction direction() const;
+
+       /**
+        * @brief Set edge detection.
+        * @param edge New edge detection setting.
+        * @return Reference to self.
+        */
+       line_settings& set_edge_detection(line::edge edge);
+
+       /**
+        * @brief Get edge detection.
+        * @return Current edge detection setting.
+        */
+       line::edge edge_detection() const;
+
+       /**
+        * @brief Set bias setting.
+        * @param bias New bias.
+        * @return Reference to self.
+        */
+       line_settings& set_bias(line::bias bias);
+
+       /**
+        * @brief Get bias setting.
+        * @return Current bias.
+        */
+       line::bias bias() const;
+
+       /**
+        * @brief Set drive setting.
+        * @param drive New drive.
+        * @return Reference to self.
+        */
+       line_settings& set_drive(line::drive drive);
+
+       /**
+        * @brief Get drive setting.
+        * @return Current drive.
+        */
+       line::drive drive() const;
+
+       /**
+        * @brief Set the active-low setting.
+        * @param active_low New active-low setting.
+        * @return Reference to self.
+        */
+       line_settings& set_active_low(bool active_low);
+
+       /**
+        * @brief Get the active-low setting.
+        * @return Current active-low setting.
+        */
+       bool active_low() const noexcept;
+
+       /**
+        * @brief Set debounce period.
+        * @param period New debounce period in microseconds.
+        * @return Reference to self.
+        */
+       line_settings& set_debounce_period(const ::std::chrono::microseconds& period);
+
+       /**
+        * @brief Get debounce period.
+        * @return Current debounce period.
+        */
+       ::std::chrono::microseconds debounce_period() const noexcept;
+
+       /**
+        * @brief Set the event clock to use for edge event timestamps.
+        * @param event_clock Clock to use.
+        * @return Reference to self.
+        */
+       line_settings& set_event_clock(line::clock event_clock);
+
+       /**
+        * @brief Get the event clock used for edge event timestamps.
+        * @return Current event clock type.
+        */
+       line::clock event_clock() const;
+
+       /**
+        * @brief Set the output value.
+        * @param value New output value.
+        * @return Reference to self.
+        */
+       line_settings& set_output_value(line::value value);
+
+       /**
+        * @brief Get the output value.
+        * @return Current output value.
+        */
+       line::value output_value() const;
+
+private:
+
+       struct impl;
+
+       ::std::unique_ptr<impl> _m_priv;
+
+       friend line_config;
+};
+
+/**
+ * @brief Stream insertion operator for line settings.
+ * @param out Output stream to write to.
+ * @param settings Line settings object to insert into the output stream.
+ * @return Reference to out.
+ */
+::std::ostream& operator<<(::std::ostream& out, const line_settings& settings);
+
+/**
+ * @}
+ */
+
+} /* namespace gpiod */
+
+#endif /* __LIBGPIOD_CXX_LINE_SETTINGS_HPP__ */
diff --git a/bindings/cxx/gpiodcxx/line.hpp b/bindings/cxx/gpiodcxx/line.hpp
new file mode 100644 (file)
index 0000000..2810571
--- /dev/null
@@ -0,0 +1,276 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/* SPDX-FileCopyrightText: 2021-2022 Bartosz Golaszewski <brgl@bgdev.pl> */
+
+/**
+ * @file line.hpp
+ */
+
+#ifndef __LIBGPIOD_CXX_LINE_HPP__
+#define __LIBGPIOD_CXX_LINE_HPP__
+
+#if !defined(__LIBGPIOD_GPIOD_CXX_INSIDE__)
+#error "Only gpiod.hpp can be included directly."
+#endif
+
+#include <ostream>
+#include <utility>
+#include <vector>
+
+namespace gpiod {
+
+/**
+ * @brief Namespace containing various type definitions for GPIO lines.
+ */
+namespace line {
+
+/**
+ * @ingroup gpiod_cxx
+ * @{
+ */
+
+/**
+ * @brief Wrapper around unsigned int for representing line offsets.
+ */
+class offset
+{
+public:
+       /**
+        * @brief Constructor with implicit conversion from unsigned int.
+        * @param off Line offset.
+        */
+       offset(unsigned int off = 0) : _m_offset(off) { }
+
+       /**
+        * @brief Copy constructor.
+        * @param other Object to copy.
+        */
+       offset(const offset& other) = default;
+
+       /**
+        * @brief Move constructor.
+        * @param other Object to move.
+        */
+       offset(offset&& other) = default;
+
+       ~offset() = default;
+
+       /**
+        * @brief Assignment operator.
+        * @param other Object to copy.
+        * @return Reference to self.
+        */
+       offset& operator=(const offset& other) = default;
+
+       /**
+        * @brief Move assignment operator.
+        * @param other Object to move.
+        * @return Reference to self.
+        */
+       offset& operator=(offset&& other) noexcept = default;
+
+       /**
+        * @brief Conversion operator to `unsigned int`.
+        */
+       operator unsigned int() const noexcept
+       {
+               return this->_m_offset;
+       }
+
+private:
+       unsigned int _m_offset;
+};
+
+/**
+ * @brief Logical line states.
+ */
+enum class value
+{
+       INACTIVE = 0,
+       /**< Line is inactive. */
+       ACTIVE = 1,
+       /**< Line is active. */
+};
+
+/**
+ * @brief Direction settings.
+ */
+enum class direction
+{
+       AS_IS = 1,
+       /**< Request the line(s), but don't change current direction. */
+       INPUT,
+       /**< Direction is input - we're reading the state of a GPIO line. */
+       OUTPUT,
+       /**< Direction is output - we're driving the GPIO line. */
+};
+
+/**
+ * @brief Edge detection settings.
+ */
+enum class edge
+{
+       NONE = 1,
+       /**< Line edge detection is disabled. */
+       RISING,
+       /**< Line detects rising edge events. */
+       FALLING,
+       /**< Line detect falling edge events. */
+       BOTH,
+       /**< Line detects both rising and falling edge events. */
+};
+
+/**
+ * @brief Internal bias settings.
+ */
+enum class bias
+{
+       AS_IS = 1,
+       /**< Don't change the bias setting when applying line config. */
+       UNKNOWN,
+       /**< The internal bias state is unknown. */
+       DISABLED,
+       /**< The internal bias is disabled. */
+       PULL_UP,
+       /**< The internal pull-up bias is enabled. */
+       PULL_DOWN,
+       /**< The internal pull-down bias is enabled. */
+};
+
+/**
+ * @brief Drive settings.
+ */
+enum class drive
+{
+       PUSH_PULL = 1,
+       /**< Drive setting is push-pull. */
+       OPEN_DRAIN,
+       /**< Line output is open-drain. */
+       OPEN_SOURCE,
+       /**< Line output is open-source. */
+};
+
+/**
+ * @brief Event clock settings.
+ */
+enum class clock
+{
+       MONOTONIC = 1,
+       /**< Line uses the monotonic clock for edge event timestamps. */
+       REALTIME,
+       /**< Line uses the realtime clock for edge event timestamps. */
+       HTE,
+       /*<< Line uses the hardware timestamp engine for event timestamps. */
+};
+
+/**
+ * @brief Vector of line offsets.
+ */
+using offsets = ::std::vector<offset>;
+
+/**
+ * @brief Vector of line values.
+ */
+using values = ::std::vector<value>;
+
+/**
+ * @brief Represents a mapping of a line offset to line logical state.
+ */
+using value_mapping = ::std::pair<offset, value>;
+
+/**
+ * @brief Vector of offset->value mappings. Each mapping is defined as a pair
+ *        of an unsigned and signed integers.
+ */
+using value_mappings = ::std::vector<value_mapping>;
+
+/**
+ * @brief Stream insertion operator for logical line values.
+ * @param out Output stream.
+ * @param val Value to insert into the output stream in a human-readable form.
+ * @return Reference to out.
+ */
+::std::ostream& operator<<(::std::ostream& out, value val);
+
+/**
+ * @brief Stream insertion operator for direction values.
+ * @param out Output stream.
+ * @param dir Value to insert into the output stream in a human-readable form.
+ * @return Reference to out.
+ */
+::std::ostream& operator<<(::std::ostream& out, direction dir);
+
+/**
+ * @brief Stream insertion operator for edge detection values.
+ * @param out Output stream.
+ * @param edge Value to insert into the output stream in a human-readable form.
+ * @return Reference to out.
+ */
+::std::ostream& operator<<(::std::ostream& out, edge edge);
+
+/**
+ * @brief Stream insertion operator for bias values.
+ * @param out Output stream.
+ * @param bias Value to insert into the output stream in a human-readable form.
+ * @return Reference to out.
+ */
+::std::ostream& operator<<(::std::ostream& out, bias bias);
+
+/**
+ * @brief Stream insertion operator for drive values.
+ * @param out Output stream.
+ * @param drive Value to insert into the output stream in a human-readable form.
+ * @return Reference to out.
+ */
+::std::ostream& operator<<(::std::ostream& out, drive drive);
+
+/**
+ * @brief Stream insertion operator for event clock values.
+ * @param out Output stream.
+ * @param clock Value to insert into the output stream in a human-readable form.
+ * @return Reference to out.
+ */
+::std::ostream& operator<<(::std::ostream& out, clock clock);
+
+/**
+ * @brief Stream insertion operator for the list of output values.
+ * @param out Output stream.
+ * @param vals Object to insert into the output stream in a human-readable form.
+ * @return Reference to out.
+ */
+::std::ostream& operator<<(::std::ostream& out, const values& vals);
+
+/**
+ * @brief Stream insertion operator for the list of line offsets.
+ * @param out Output stream.
+ * @param offs Object to insert into the output stream in a human-readable form.
+ * @return Reference to out.
+ */
+::std::ostream& operator<<(::std::ostream& out, const offsets& offs);
+
+/**
+ * @brief Stream insertion operator for the offset-to-value mapping.
+ * @param out Output stream.
+ * @param mapping Value to insert into the output stream in a human-readable
+ *        form.
+ * @return Reference to out.
+ */
+::std::ostream& operator<<(::std::ostream& out, const value_mapping& mapping);
+
+/**
+ * @brief Stream insertion operator for the list of offset-to-value mappings.
+ * @param out Output stream.
+ * @param mappings Object to insert into the output stream in a human-readable
+ *        form.
+ * @return Reference to out.
+ */
+::std::ostream& operator<<(::std::ostream& out, const value_mappings& mappings);
+
+/**
+ * @}
+ */
+
+} /* namespace line */
+
+} /* namespace gpiod */
+
+#endif /* __LIBGPIOD_CXX_LINE_HPP__ */
diff --git a/bindings/cxx/gpiodcxx/misc.hpp b/bindings/cxx/gpiodcxx/misc.hpp
new file mode 100644 (file)
index 0000000..eab8eba
--- /dev/null
@@ -0,0 +1,44 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/* SPDX-FileCopyrightText: 2021 Bartosz Golaszewski <brgl@bgdev.pl> */
+
+/**
+ * @file misc.hpp
+ */
+
+#ifndef __LIBGPIOD_CXX_MISC_HPP__
+#define __LIBGPIOD_CXX_MISC_HPP__
+
+#if !defined(__LIBGPIOD_GPIOD_CXX_INSIDE__)
+#error "Only gpiod.hpp can be included directly."
+#endif
+
+#include <string>
+
+namespace gpiod {
+
+/**
+ * @ingroup gpiod_cxx
+ * @{
+ */
+
+/**
+ * @brief Check if the file pointed to by path is a GPIO chip character device.
+ * @param path Path to check.
+ * @return True if the file exists and is a GPIO chip character device or a
+ *         symbolic link to it.
+ */
+bool is_gpiochip_device(const ::std::filesystem::path& path);
+
+/**
+ * @brief Get the human readable version string for libgpiod API
+ * @return String containing the library version.
+ */
+const ::std::string& api_version();
+
+/**
+ * @}
+ */
+
+} /* namespace gpiod */
+
+#endif /* __LIBGPIOD_CXX_MISC_HPP__ */
diff --git a/bindings/cxx/gpiodcxx/request-builder.hpp b/bindings/cxx/gpiodcxx/request-builder.hpp
new file mode 100644 (file)
index 0000000..192bd91
--- /dev/null
@@ -0,0 +1,157 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/* SPDX-FileCopyrightText: 2022 Bartosz Golaszewski <brgl@bgdev.pl> */
+
+/**
+ * @file request-builder.hpp
+ */
+
+#ifndef __LIBGPIOD_CXX_REQUEST_BUILDER_HPP__
+#define __LIBGPIOD_CXX_REQUEST_BUILDER_HPP__
+
+#if !defined(__LIBGPIOD_GPIOD_CXX_INSIDE__)
+#error "Only gpiod.hpp can be included directly."
+#endif
+
+#include <memory>
+#include <ostream>
+
+namespace gpiod {
+
+class chip;
+class line_config;
+class line_request;
+class request_config;
+
+/**
+ * @ingroup gpiod_cxx
+ * @{
+ */
+
+/**
+ * @brief Intermediate object storing the configuration for a line request.
+ */
+class request_builder final
+{
+public:
+
+       request_builder(const request_builder& other) = delete;
+
+       /**
+        * @brief Move constructor.
+        * @param other Object to be moved.
+        */
+       request_builder(request_builder&& other) noexcept;
+
+       ~request_builder();
+
+       request_builder& operator=(const request_builder& other) = delete;
+
+       /**
+        * @brief Move assignment operator.
+        * @param other Object to be moved.
+        * @return Reference to self.
+        */
+       request_builder& operator=(request_builder&& other) noexcept;
+
+       /**
+        * @brief Set the request config for the request.
+        * @param req_cfg Request config to use.
+        * @return Reference to self.
+        */
+       request_builder& set_request_config(request_config& req_cfg);
+
+       /**
+        * @brief Get the current request config.
+        * @return Const reference to the current request config stored by this
+        *         object.
+        */
+       const request_config& get_request_config() const noexcept;
+
+       /**
+        * @brief Set consumer in the request config stored by this object.
+        * @param consumer New consumer string.
+        * @return Reference to self.
+        */
+       request_builder& set_consumer(const ::std::string& consumer) noexcept;
+
+       /**
+        * @brief Set the event buffer size in the request config stored by
+        *        this object.
+        * @param event_buffer_size New event buffer size.
+        * @return Reference to self.
+        */
+       request_builder& set_event_buffer_size(::std::size_t event_buffer_size) noexcept;
+
+       /**
+        * @brief Set the line config for this request.
+        * @param line_cfg Line config to use.
+        * @return Reference to self.
+        */
+       request_builder& set_line_config(line_config &line_cfg);
+
+       /**
+        * @brief Get the current line config.
+        * @return Const reference to the current line config stored by this
+        *         object.
+        */
+       const line_config& get_line_config() const noexcept;
+
+       /**
+        * @brief Add line settings to the line config stored by this object
+        *        for a single offset.
+        * @param offset Offset for which to add settings.
+        * @param settings Line settings to use.
+        * @return Reference to self.
+        */
+       request_builder& add_line_settings(line::offset offset, const line_settings& settings);
+
+       /**
+        * @brief Add line settings to the line config stored by this object
+        *        for a set of offsets.
+        * @param offsets Offsets for which to add settings.
+        * @param settings Settings to add.
+        * @return Reference to self.
+        */
+       request_builder& add_line_settings(const line::offsets& offsets, const line_settings& settings);
+
+       /**
+        * @brief Set output values for a number of lines in the line config
+        *        stored by this object.
+        * @param values Buffer containing the output values.
+        * @return Reference to self.
+        */
+       request_builder& set_output_values(const line::values& values);
+
+       /**
+        * @brief Make the line request.
+        * @return New line_request object.
+        */
+       line_request do_request();
+
+private:
+
+       struct impl;
+
+       request_builder(chip& chip);
+
+       ::std::unique_ptr<impl> _m_priv;
+
+       friend chip;
+       friend ::std::ostream& operator<<(::std::ostream& out, const request_builder& builder);
+};
+
+/**
+ * @brief Stream insertion operator for GPIO request builder objects.
+ * @param out Output stream to write to.
+ * @param builder Request builder object to insert into the output stream.
+ * @return Reference to out.
+ */
+::std::ostream& operator<<(::std::ostream& out, const request_builder& builder);
+
+/**
+ * @}
+ */
+
+} /* namespace gpiod */
+
+#endif /* __LIBGPIOD_CXX_REQUEST_BUILDER_HPP__ */
diff --git a/bindings/cxx/gpiodcxx/request-config.hpp b/bindings/cxx/gpiodcxx/request-config.hpp
new file mode 100644 (file)
index 0000000..6ebbf99
--- /dev/null
@@ -0,0 +1,114 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/* SPDX-FileCopyrightText: 2021-2022 Bartosz Golaszewski <brgl@bgdev.pl> */
+
+/**
+ * @file request-config.hpp
+ */
+
+#ifndef __LIBGPIOD_CXX_REQUEST_CONFIG_HPP__
+#define __LIBGPIOD_CXX_REQUEST_CONFIG_HPP__
+
+#if !defined(__LIBGPIOD_GPIOD_CXX_INSIDE__)
+#error "Only gpiod.hpp can be included directly."
+#endif
+
+#include <cstddef>
+#include <iostream>
+#include <memory>
+#include <string>
+
+#include "line.hpp"
+
+namespace gpiod {
+
+class chip;
+
+/**
+ * @ingroup gpiod_cxx
+ * @{
+ */
+
+/**
+ * @brief Stores a set of options passed to the kernel when making a line
+ *        request.
+ */
+class request_config final
+{
+public:
+
+       /**
+        * @brief Constructor.
+        */
+       request_config();
+
+       request_config(const request_config& other) = delete;
+
+       /**
+        * @brief Move constructor.
+        * @param other Object to move.
+        */
+       request_config(request_config&& other) noexcept;
+
+       ~request_config();
+
+       /**
+        * @brief Move assignment operator.
+        * @param other Object to move.
+        * @return Reference to self.
+        */
+       request_config& operator=(request_config&& other) noexcept;
+
+       /**
+        * @brief Set the consumer name.
+        * @param consumer New consumer name.
+        * @return Reference to self.
+        */
+       request_config& set_consumer(const ::std::string& consumer) noexcept;
+
+       /**
+        * @brief Get the consumer name.
+        * @return Currently configured consumer name. May be an empty string.
+        */
+       ::std::string consumer() const noexcept;
+
+       /**
+        * @brief Set the size of the kernel event buffer.
+        * @param event_buffer_size New event buffer size.
+        * @return Reference to self.
+        * @note The kernel may adjust the value if it's too high. If set to 0,
+        *       the default value will be used.
+        */
+       request_config& set_event_buffer_size(::std::size_t event_buffer_size) noexcept;
+
+       /**
+        * @brief Get the edge event buffer size from this request config.
+        * @return Current edge event buffer size setting.
+        */
+       ::std::size_t event_buffer_size() const noexcept;
+
+private:
+
+       struct impl;
+
+       ::std::shared_ptr<impl> _m_priv;
+
+       request_config& operator=(const request_config& other);
+
+       friend request_builder;
+};
+
+/**
+ * @brief Stream insertion operator for request_config objects.
+ * @param out Output stream to write to.
+ * @param config request_config to insert into the output stream.
+ * @return Reference to out.
+ */
+::std::ostream& operator<<(::std::ostream& out, const request_config& config);
+
+/**
+ * @}
+ */
+
+} /* namespace gpiod */
+
+#endif /* __LIBGPIOD_CXX_REQUEST_CONFIG_HPP__ */
diff --git a/bindings/cxx/gpiodcxx/timestamp.hpp b/bindings/cxx/gpiodcxx/timestamp.hpp
new file mode 100644 (file)
index 0000000..fcb4d8d
--- /dev/null
@@ -0,0 +1,123 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/* SPDX-FileCopyrightText: 2022 Bartosz Golaszewski <brgl@bgdev.pl> */
+
+/**
+ * @file timestamp.hpp
+ */
+
+#ifndef __LIBGPIOD_CXX_TIMESTAMP_HPP__
+#define __LIBGPIOD_CXX_TIMESTAMP_HPP__
+
+#if !defined(__LIBGPIOD_GPIOD_CXX_INSIDE__)
+#error "Only gpiod.hpp can be included directly."
+#endif
+
+#include <chrono>
+#include <cstdint>
+
+namespace gpiod {
+
+/**
+ * @ingroup gpiod_cxx
+ * @{
+ */
+
+/**
+ * @brief Stores the edge and info event timestamps as returned by the kernel
+ *        and allows to convert them to std::chrono::time_point.
+ */
+class timestamp final
+{
+public:
+
+       /**
+        * @brief Monotonic time_point.
+        */
+       using time_point_monotonic = ::std::chrono::time_point<::std::chrono::steady_clock>;
+
+       /**
+        * @brief Real-time time_point.
+        */
+       using time_point_realtime = ::std::chrono::time_point<::std::chrono::system_clock,
+                                                             ::std::chrono::nanoseconds>;
+
+       /**
+        * @brief Constructor with implicit  conversion from `uint64_t`.
+        * @param ns Timestamp in nanoseconds.
+        */
+       timestamp(::std::uint64_t ns) : _m_ns(ns) { }
+
+       /**
+        * @brief Copy constructor.
+        * @param other Object to copy.
+        */
+       timestamp(const timestamp& other) noexcept = default;
+
+       /**
+        * @brief Move constructor.
+        * @param other Object to move.
+        */
+       timestamp(timestamp&& other) noexcept = default;
+
+       /**
+        * @brief Assignment operator.
+        * @param other Object to copy.
+        * @return Reference to self.
+        */
+       timestamp& operator=(const timestamp& other) noexcept = default;
+
+       /**
+        * @brief Move assignment operator.
+        * @param other Object to move.
+        * @return Reference to self.
+        */
+       timestamp& operator=(timestamp&& other) noexcept = default;
+
+       ~timestamp() = default;
+
+       /**
+        * @brief Conversion operator to `std::uint64_t`.
+        */
+       operator ::std::uint64_t() noexcept
+       {
+               return this->ns();
+       }
+
+       /**
+        * @brief Get the timestamp in nanoseconds.
+        * @return Timestamp in nanoseconds.
+        */
+       ::std::uint64_t ns() const noexcept
+       {
+               return this->_m_ns;
+       }
+
+       /**
+        * @brief Convert the timestamp to a monotonic time_point.
+        * @return time_point associated with the steady clock.
+        */
+       time_point_monotonic to_time_point_monotonic() const
+       {
+               return time_point_monotonic(::std::chrono::nanoseconds(this->ns()));
+       }
+
+       /**
+        * @brief Convert the timestamp to a real-time time_point.
+        * @return time_point associated with the system clock.
+        */
+       time_point_realtime to_time_point_realtime() const
+       {
+               return time_point_realtime(::std::chrono::nanoseconds(this->ns()));
+       }
+
+private:
+       ::std::uint64_t _m_ns;
+};
+
+/**
+ * @}
+ */
+
+} /* namespace gpiod */
+
+#endif /* __LIBGPIOD_CXX_TIMESTAMP_HPP__ */
diff --git a/bindings/cxx/info-event.cpp b/bindings/cxx/info-event.cpp
new file mode 100644 (file)
index 0000000..1f6d0d7
--- /dev/null
@@ -0,0 +1,103 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// SPDX-FileCopyrightText: 2021-2022 Bartosz Golaszewski <brgl@bgdev.pl>
+
+#include <map>
+#include <ostream>
+
+#include "internal.hpp"
+
+namespace gpiod {
+
+namespace {
+
+const ::std::map<int, info_event::event_type> event_type_mapping = {
+       { GPIOD_INFO_EVENT_LINE_REQUESTED,      info_event::event_type::LINE_REQUESTED },
+       { GPIOD_INFO_EVENT_LINE_RELEASED,       info_event::event_type::LINE_RELEASED },
+       { GPIOD_INFO_EVENT_LINE_CONFIG_CHANGED, info_event::event_type::LINE_CONFIG_CHANGED },
+};
+
+const ::std::map<info_event::event_type, ::std::string> event_type_names = {
+       { info_event::event_type::LINE_REQUESTED,       "LINE_REQUESTED" },
+       { info_event::event_type::LINE_RELEASED,        "LINE_RELEASED" },
+       { info_event::event_type::LINE_CONFIG_CHANGED,  "LINE_CONFIG_CHANGED" },
+};
+
+} /* namespace */
+
+void info_event::impl::set_info_event_ptr(info_event_ptr& new_event)
+{
+       ::gpiod_line_info* info = ::gpiod_info_event_get_line_info(new_event.get());
+
+       line_info_ptr copy(::gpiod_line_info_copy(info));
+       if (!copy)
+               throw_from_errno("unable to copy the line info object");
+
+       this->event = ::std::move(new_event);
+       this->info._m_priv->set_info_ptr(copy);
+}
+
+info_event::info_event()
+       : _m_priv(new impl)
+{
+
+}
+
+GPIOD_CXX_API info_event::info_event(const info_event& other)
+       : _m_priv(other._m_priv)
+{
+
+}
+
+GPIOD_CXX_API info_event::info_event(info_event&& other) noexcept
+       : _m_priv(::std::move(other._m_priv))
+{
+
+}
+
+GPIOD_CXX_API info_event::~info_event()
+{
+
+}
+
+GPIOD_CXX_API info_event& info_event::operator=(const info_event& other)
+{
+       this->_m_priv = other._m_priv;
+
+       return *this;
+}
+
+GPIOD_CXX_API info_event& info_event::operator=(info_event&& other) noexcept
+{
+       this->_m_priv = ::std::move(other._m_priv);
+
+       return *this;
+}
+
+GPIOD_CXX_API info_event::event_type info_event::type() const
+{
+       int type = ::gpiod_info_event_get_event_type(this->_m_priv->event.get());
+
+       return get_mapped_value(type, event_type_mapping);
+}
+
+GPIOD_CXX_API ::std::uint64_t info_event::timestamp_ns() const noexcept
+{
+       return ::gpiod_info_event_get_timestamp_ns(this->_m_priv->event.get());
+}
+
+GPIOD_CXX_API const line_info& info_event::get_line_info() const noexcept
+{
+       return this->_m_priv->info;
+}
+
+GPIOD_CXX_API ::std::ostream& operator<<(::std::ostream& out, const info_event& event)
+{
+       out << "gpiod::info_event(event_type='" << event_type_names.at(event.type()) <<
+              "', timestamp=" << event.timestamp_ns() <<
+              ", line_info=" << event.get_line_info() <<
+              ")";
+
+       return out;
+}
+
+} /* namespace gpiod */
diff --git a/bindings/cxx/internal.cpp b/bindings/cxx/internal.cpp
new file mode 100644 (file)
index 0000000..237d5d5
--- /dev/null
@@ -0,0 +1,28 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// SPDX-FileCopyrightText: 2021-2022 Bartosz Golaszewski <brgl@bgdev.pl>
+
+#include <map>
+#include <stdexcept>
+#include <system_error>
+
+#include "internal.hpp"
+
+namespace gpiod {
+
+void throw_from_errno(const ::std::string& what)
+{
+       switch (errno) {
+       case EINVAL:
+               throw ::std::invalid_argument(what);
+       case E2BIG:
+               throw ::std::length_error(what);
+       case ENOMEM:
+               throw ::std::bad_alloc();
+       case EDOM:
+               throw ::std::domain_error(what);
+       default:
+               throw ::std::system_error(errno, ::std::system_category(), what);
+       }
+}
+
+} /* namespace gpiod */
diff --git a/bindings/cxx/internal.hpp b/bindings/cxx/internal.hpp
new file mode 100644 (file)
index 0000000..b64daf1
--- /dev/null
@@ -0,0 +1,234 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/* SPDX-FileCopyrightText: 2021-2022 Bartosz Golaszewski <brgl@bgdev.pl> */
+
+#ifndef __LIBGPIOD_CXX_INTERNAL_HPP__
+#define __LIBGPIOD_CXX_INTERNAL_HPP__
+
+#include <gpiod.h>
+#include <map>
+#include <memory>
+#include <string>
+#include <utility>
+#include <vector>
+
+#include "gpiod.hpp"
+
+namespace gpiod {
+
+template<class cxx_enum_type, class c_enum_type>
+cxx_enum_type get_mapped_value(c_enum_type value,
+                              const ::std::map<c_enum_type, cxx_enum_type>& mapping)
+{
+       try {
+               return mapping.at(value);
+       } catch (const ::std::out_of_range& err) {
+               /* FIXME Demangle the name. */
+               throw bad_mapping(::std::string("invalid value for ") +
+                                 typeid(cxx_enum_type).name());
+       }
+}
+
+void throw_from_errno(const ::std::string& what);
+::gpiod_line_value map_output_value(line::value value);
+
+template<class T, void F(T*)> struct deleter
+{
+       void operator()(T* ptr)
+       {
+               F(ptr);
+       }
+};
+
+using chip_deleter = deleter<::gpiod_chip, ::gpiod_chip_close>;
+using chip_info_deleter = deleter<::gpiod_chip_info, ::gpiod_chip_info_free>;
+using line_info_deleter = deleter<::gpiod_line_info, ::gpiod_line_info_free>;
+using info_event_deleter = deleter<::gpiod_info_event, ::gpiod_info_event_free>;
+using line_settings_deleter = deleter<::gpiod_line_settings, ::gpiod_line_settings_free>;
+using line_config_deleter = deleter<::gpiod_line_config, ::gpiod_line_config_free>;
+using request_config_deleter = deleter<::gpiod_request_config, ::gpiod_request_config_free>;
+using line_request_deleter = deleter<::gpiod_line_request, ::gpiod_line_request_release>;
+using edge_event_deleter = deleter<::gpiod_edge_event, ::gpiod_edge_event_free>;
+using edge_event_buffer_deleter = deleter<::gpiod_edge_event_buffer,
+                                         ::gpiod_edge_event_buffer_free>;
+
+using chip_ptr = ::std::unique_ptr<::gpiod_chip, chip_deleter>;
+using chip_info_ptr = ::std::unique_ptr<::gpiod_chip_info, chip_info_deleter>;
+using line_info_ptr = ::std::unique_ptr<::gpiod_line_info, line_info_deleter>;
+using info_event_ptr = ::std::unique_ptr<::gpiod_info_event, info_event_deleter>;
+using line_settings_ptr = ::std::unique_ptr<::gpiod_line_settings, line_settings_deleter>;
+using line_config_ptr = ::std::unique_ptr<::gpiod_line_config, line_config_deleter>;
+using request_config_ptr = ::std::unique_ptr<::gpiod_request_config, request_config_deleter>;
+using line_request_ptr = ::std::unique_ptr<::gpiod_line_request, line_request_deleter>;
+using edge_event_ptr = ::std::unique_ptr<::gpiod_edge_event, edge_event_deleter>;
+using edge_event_buffer_ptr = ::std::unique_ptr<::gpiod_edge_event_buffer,
+                                               edge_event_buffer_deleter>;
+
+struct chip::impl
+{
+       impl(const ::std::filesystem::path& path);
+       impl(const impl& other) = delete;
+       impl(impl&& other) = delete;
+       impl& operator=(const impl& other) = delete;
+       impl& operator=(impl&& other) = delete;
+
+       void throw_if_closed() const;
+
+       chip_ptr chip;
+};
+
+struct chip_info::impl
+{
+       impl() = default;
+       impl(const impl& other) = delete;
+       impl(impl&& other) = delete;
+       impl& operator=(const impl& other) = delete;
+       impl& operator=(impl&& other) = delete;
+
+       void set_info_ptr(chip_info_ptr& new_info);
+
+       chip_info_ptr info;
+};
+
+struct line_info::impl
+{
+       impl() = default;
+       impl(const impl& other) = delete;
+       impl(impl&& other) = delete;
+       impl& operator=(const impl& other) = delete;
+       impl& operator=(impl&& other) = delete;
+
+       void set_info_ptr(line_info_ptr& new_info);
+
+       line_info_ptr info;
+};
+
+struct info_event::impl
+{
+       impl() = default;
+       impl(const impl& other) = delete;
+       impl(impl&& other) = delete;
+       impl& operator=(const impl& other) = delete;
+       impl& operator=(impl&& other) = delete;
+
+       void set_info_event_ptr(info_event_ptr& new_event);
+
+       info_event_ptr event;
+       line_info info;
+};
+
+struct line_settings::impl
+{
+       impl();
+       impl(const impl& other);
+       impl(impl&& other) = delete;
+       impl& operator=(const impl& other) = delete;
+       impl& operator=(impl&& other) = delete;
+
+       line_settings_ptr settings;
+};
+
+struct line_config::impl
+{
+       impl();
+       impl(const impl& other) = delete;
+       impl(impl&& other) = delete;
+       impl& operator=(const impl& other) = delete;
+       impl& operator=(impl&& other) = delete;
+
+       line_config_ptr config;
+};
+
+struct request_config::impl
+{
+       impl();
+       impl(const impl& other) = delete;
+       impl(impl&& other) = delete;
+       impl& operator=(const impl& other) = delete;
+       impl& operator=(impl&& other) = delete;
+
+       request_config_ptr config;
+};
+
+struct line_request::impl
+{
+       impl() = default;
+       impl(const impl& other) = delete;
+       impl(impl&& other) = delete;
+       impl& operator=(const impl& other) = delete;
+       impl& operator=(impl&& other) = delete;
+
+       void throw_if_released() const;
+       void set_request_ptr(line_request_ptr& ptr);
+       void fill_offset_buf(const line::offsets& offsets);
+
+       line_request_ptr request;
+
+       /*
+        * Used when reading/setting the line values in order to avoid
+        * allocating a new buffer on every call. We're not doing it for
+        * offsets in the line & request config structures because they don't
+        * require high performance unlike the set/get value calls.
+        */
+       ::std::vector<unsigned int> offset_buf;
+};
+
+struct edge_event::impl
+{
+       impl() = default;
+       impl(const impl& other) = delete;
+       impl(impl&& other) = delete;
+       virtual ~impl() = default;
+       impl& operator=(const impl& other) = delete;
+       impl& operator=(impl&& other) = delete;
+
+       virtual ::gpiod_edge_event* get_event_ptr() const noexcept = 0;
+       virtual ::std::shared_ptr<impl> copy(const ::std::shared_ptr<impl>& self) const = 0;
+};
+
+struct edge_event::impl_managed final : public edge_event::impl
+{
+       impl_managed() = default;
+       impl_managed(const impl_managed& other) = delete;
+       impl_managed(impl_managed&& other) = delete;
+       ~impl_managed() = default;
+       impl_managed& operator=(const impl_managed& other) = delete;
+       impl_managed& operator=(impl_managed&& other) = delete;
+
+       ::gpiod_edge_event* get_event_ptr() const noexcept override;
+       ::std::shared_ptr<impl> copy(const ::std::shared_ptr<impl>& self) const override;
+
+       edge_event_ptr event;
+};
+
+struct edge_event::impl_external final : public edge_event::impl
+{
+       impl_external();
+       impl_external(const impl_external& other) = delete;
+       impl_external(impl_external&& other) = delete;
+       ~impl_external() = default;
+       impl_external& operator=(const impl_external& other) = delete;
+       impl_external& operator=(impl_external&& other) = delete;
+
+       ::gpiod_edge_event* get_event_ptr() const noexcept override;
+       ::std::shared_ptr<impl> copy(const ::std::shared_ptr<impl>& self) const override;
+
+       ::gpiod_edge_event *event;
+};
+
+struct edge_event_buffer::impl
+{
+       impl(unsigned int capacity);
+       impl(const impl& other) = delete;
+       impl(impl&& other) = delete;
+       impl& operator=(const impl& other) = delete;
+       impl& operator=(impl&& other) = delete;
+
+       int read_events(const line_request_ptr& request, unsigned int max_events);
+
+       edge_event_buffer_ptr buffer;
+       ::std::vector<edge_event> events;
+};
+
+} /* namespace gpiod */
+
+#endif /* __LIBGPIOD_CXX_INTERNAL_HPP__ */
diff --git a/bindings/cxx/libgpiodcxx.pc.in b/bindings/cxx/libgpiodcxx.pc.in
new file mode 100644 (file)
index 0000000..731227c
--- /dev/null
@@ -0,0 +1,14 @@
+# SPDX-License-Identifier: GPL-2.0-or-later
+# SPDX-FileCopyrightText: 2017-2021 Bartosz Golaszewski <bartekgola@gmail.com>
+
+prefix=@prefix@
+exec_prefix=@exec_prefix@
+libdir=@libdir@
+includedir=@includedir@
+
+Name: libgpiodcxx
+Description: C++ bindings for libgpiod
+URL: @PACKAGE_URL@
+Version: @PACKAGE_VERSION@
+Libs: -L${libdir} -lgpiodcxx
+Cflags: -I${includedir}
diff --git a/bindings/cxx/line-config.cpp b/bindings/cxx/line-config.cpp
new file mode 100644 (file)
index 0000000..7e3dc8c
--- /dev/null
@@ -0,0 +1,167 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// SPDX-FileCopyrightText: 2021-2022 Bartosz Golaszewski <brgl@bgdev.pl>
+
+#include <cstdlib>
+#include <iterator>
+#include <ostream>
+#include <sstream>
+#include <utility>
+#include <vector>
+
+#include "internal.hpp"
+
+namespace gpiod {
+
+namespace {
+
+line_config_ptr make_line_config()
+{
+       line_config_ptr config(::gpiod_line_config_new());
+       if (!config)
+               throw_from_errno("Unable to allocate the line config object");
+
+       return config;
+}
+
+} /* namespace */
+
+line_config::impl::impl()
+       : config(make_line_config())
+{
+
+}
+
+GPIOD_CXX_API line_config::line_config()
+       : _m_priv(new impl)
+{
+
+}
+
+GPIOD_CXX_API line_config::line_config(line_config&& other) noexcept
+       : _m_priv(::std::move(other._m_priv))
+{
+
+}
+
+GPIOD_CXX_API line_config::~line_config()
+{
+
+}
+
+line_config& line_config::operator=(const line_config& other)
+{
+       this->_m_priv = other._m_priv;
+
+       return *this;
+}
+
+GPIOD_CXX_API line_config& line_config::operator=(line_config&& other) noexcept
+{
+       this->_m_priv = ::std::move(other._m_priv);
+
+       return *this;
+}
+
+GPIOD_CXX_API line_config& line_config::reset() noexcept
+{
+       ::gpiod_line_config_reset(this->_m_priv->config.get());
+
+       return *this;
+}
+
+GPIOD_CXX_API line_config& line_config::add_line_settings(line::offset offset,
+                                                         const line_settings& settings)
+{
+       return this->add_line_settings(line::offsets({offset}), settings);
+}
+
+GPIOD_CXX_API line_config& line_config::add_line_settings(const line::offsets& offsets,
+                                                         const line_settings& settings)
+{
+       ::std::vector<unsigned int> raw_offsets(offsets.size());
+
+       for (unsigned int i = 0; i < offsets.size(); i++)
+               raw_offsets[i] = offsets[i];
+
+       auto ret = ::gpiod_line_config_add_line_settings(this->_m_priv->config.get(),
+                                                        raw_offsets.data(), raw_offsets.size(),
+                                                        settings._m_priv->settings.get());
+       if (ret)
+               throw_from_errno("unable to add line settings");
+
+       return *this;
+}
+
+GPIOD_CXX_API line_config& line_config::set_output_values(const line::values& values)
+{
+       ::std::vector<::gpiod_line_value> mapped_values(values.size());
+
+       for (unsigned int i = 0; i < values.size(); i++)
+               mapped_values[i] = map_output_value(values[i]);
+
+       auto ret = ::gpiod_line_config_set_output_values(this->_m_priv->config.get(),
+                                                        mapped_values.data(), mapped_values.size());
+       if (ret)
+               throw_from_errno("unable to set output values");
+
+       return *this;
+}
+
+GPIOD_CXX_API ::std::map<line::offset, line_settings> line_config::get_line_settings() const
+{
+       ::std::size_t num_offsets = ::gpiod_line_config_get_num_configured_offsets(
+                                                               this->_m_priv->config.get());
+       ::std::map<line::offset, line_settings> settings_map;
+       ::std::vector<unsigned int> offsets(num_offsets);
+
+       if (num_offsets == 0)
+               return settings_map;
+
+       ::gpiod_line_config_get_configured_offsets(this->_m_priv->config.get(),
+                                       offsets.data(), num_offsets);
+
+       for (size_t i = 0; i < num_offsets; i++) {
+               line_settings settings;
+
+               settings._m_priv->settings.reset(::gpiod_line_config_get_line_settings(
+                                                       this->_m_priv->config.get(),
+                                                       offsets[i]));
+               if (!settings._m_priv->settings)
+                       throw_from_errno("unable to retrieve line settings");
+
+               settings_map[offsets[i]] = ::std::move(settings);
+       }
+
+       return settings_map;
+}
+
+GPIOD_CXX_API ::std::ostream&
+operator<<(::std::ostream& out, const line_config& config)
+{
+       auto settings_map = config.get_line_settings();
+       ::std::vector<::std::string> vec;
+
+       out << "gpiod::line_config(num_settings=" << settings_map.size();
+
+       if (settings_map.size() == 0) {
+               out << ")";
+               return out;
+       }
+
+       for (const auto& [offset, settings]: settings_map) {
+               ::std::stringstream str;
+
+               str << offset << ": " << settings;
+               vec.push_back(str.str());
+       }
+
+       out << ", settings=[";
+       ::std::copy(vec.begin(), ::std::prev(vec.end()),
+                   ::std::ostream_iterator<::std::string>(out, ", "));
+       out << vec.back();
+       out << "])";
+
+       return out;
+}
+
+} /* namespace gpiod */
diff --git a/bindings/cxx/line-info.cpp b/bindings/cxx/line-info.cpp
new file mode 100644 (file)
index 0000000..2117f68
--- /dev/null
@@ -0,0 +1,191 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// SPDX-FileCopyrightText: 2021-2022 Bartosz Golaszewski <brgl@bgdev.pl>
+
+#include <map>
+#include <ostream>
+#include <utility>
+
+#include "internal.hpp"
+
+namespace gpiod {
+
+namespace {
+
+const ::std::map<int, line::direction> direction_mapping = {
+       { GPIOD_LINE_DIRECTION_INPUT,           line::direction::INPUT },
+       { GPIOD_LINE_DIRECTION_OUTPUT,          line::direction::OUTPUT },
+};
+
+const ::std::map<int, line::bias> bias_mapping = {
+       { GPIOD_LINE_BIAS_UNKNOWN,              line::bias::UNKNOWN },
+       { GPIOD_LINE_BIAS_DISABLED,             line::bias::DISABLED },
+       { GPIOD_LINE_BIAS_PULL_UP,              line::bias::PULL_UP },
+       { GPIOD_LINE_BIAS_PULL_DOWN,            line::bias::PULL_DOWN },
+};
+
+const ::std::map<int, line::drive> drive_mapping = {
+       { GPIOD_LINE_DRIVE_PUSH_PULL,           line::drive::PUSH_PULL },
+       { GPIOD_LINE_DRIVE_OPEN_DRAIN,          line::drive::OPEN_DRAIN },
+       { GPIOD_LINE_DRIVE_OPEN_SOURCE,         line::drive::OPEN_SOURCE },
+};
+
+const ::std::map<int, line::edge> edge_mapping = {
+       { GPIOD_LINE_EDGE_NONE,                 line::edge::NONE },
+       { GPIOD_LINE_EDGE_RISING,               line::edge::RISING },
+       { GPIOD_LINE_EDGE_FALLING,              line::edge::FALLING },
+       { GPIOD_LINE_EDGE_BOTH,                 line::edge::BOTH },
+};
+
+const ::std::map<int, line::clock> clock_mapping = {
+       { GPIOD_LINE_CLOCK_MONOTONIC,           line::clock::MONOTONIC },
+       { GPIOD_LINE_CLOCK_REALTIME,            line::clock::REALTIME },
+       { GPIOD_LINE_CLOCK_HTE,                 line::clock::HTE },
+};
+
+} /* namespace */
+
+void line_info::impl::set_info_ptr(line_info_ptr& new_info)
+{
+       this->info = ::std::move(new_info);
+}
+
+line_info::line_info()
+       : _m_priv(new impl)
+{
+
+}
+
+GPIOD_CXX_API line_info::line_info(const line_info& other) noexcept
+       : _m_priv(other._m_priv)
+{
+
+}
+
+GPIOD_CXX_API line_info::line_info(line_info&& other) noexcept
+       : _m_priv(::std::move(other._m_priv))
+{
+
+}
+
+GPIOD_CXX_API line_info::~line_info()
+{
+
+}
+
+GPIOD_CXX_API line_info& line_info::operator=(const line_info& other) noexcept
+{
+       this->_m_priv = other._m_priv;
+
+       return *this;
+}
+
+GPIOD_CXX_API line_info& line_info::operator=(line_info&& other) noexcept
+{
+       this->_m_priv = ::std::move(other._m_priv);
+
+       return *this;
+}
+
+GPIOD_CXX_API line::offset line_info::offset() const noexcept
+{
+       return ::gpiod_line_info_get_offset(this->_m_priv->info.get());
+}
+
+GPIOD_CXX_API ::std::string line_info::name() const noexcept
+{
+       const char* name = ::gpiod_line_info_get_name(this->_m_priv->info.get());
+
+       return name ?: "";
+}
+
+GPIOD_CXX_API bool line_info::used() const noexcept
+{
+       return ::gpiod_line_info_is_used(this->_m_priv->info.get());
+}
+
+GPIOD_CXX_API ::std::string line_info::consumer() const noexcept
+{
+       const char* consumer = ::gpiod_line_info_get_consumer(this->_m_priv->info.get());
+
+       return consumer ?: "";
+}
+
+GPIOD_CXX_API line::direction line_info::direction() const
+{
+       int direction = ::gpiod_line_info_get_direction(this->_m_priv->info.get());
+
+       return get_mapped_value(direction, direction_mapping);
+}
+
+GPIOD_CXX_API bool line_info::active_low() const noexcept
+{
+       return ::gpiod_line_info_is_active_low(this->_m_priv->info.get());
+}
+
+GPIOD_CXX_API line::bias line_info::bias() const
+{
+       int bias = ::gpiod_line_info_get_bias(this->_m_priv->info.get());
+
+       return bias_mapping.at(bias);
+}
+
+GPIOD_CXX_API line::drive line_info::drive() const
+{
+       int drive = ::gpiod_line_info_get_drive(this->_m_priv->info.get());
+
+       return drive_mapping.at(drive);
+}
+
+GPIOD_CXX_API line::edge line_info::edge_detection() const
+{
+       int edge = ::gpiod_line_info_get_edge_detection(this->_m_priv->info.get());
+
+       return edge_mapping.at(edge);
+}
+
+GPIOD_CXX_API line::clock line_info::event_clock() const
+{
+       int clock = ::gpiod_line_info_get_event_clock(this->_m_priv->info.get());
+
+       return clock_mapping.at(clock);
+}
+
+GPIOD_CXX_API bool line_info::debounced() const  noexcept
+{
+       return ::gpiod_line_info_is_debounced(this->_m_priv->info.get());
+}
+
+GPIOD_CXX_API ::std::chrono::microseconds line_info::debounce_period() const  noexcept
+{
+       return ::std::chrono::microseconds(
+                       ::gpiod_line_info_get_debounce_period_us(this->_m_priv->info.get()));
+}
+
+GPIOD_CXX_API ::std::ostream& operator<<(::std::ostream& out, const line_info& info)
+{
+       ::std::string name, consumer;
+
+       name = info.name().empty() ? "unnamed" : ::std::string("'") + info.name() + "'";
+       consumer = info.consumer().empty() ? "unused" : ::std::string("'") + info.name() + "'";
+
+       out << "gpiod::line_info(offset=" << info.offset() <<
+              ", name=" << name <<
+              ", used=" << ::std::boolalpha << info.used() <<
+              ", consumer=" << consumer <<
+              ", direction=" << info.direction() <<
+              ", active_low=" << ::std::boolalpha << info.active_low() <<
+              ", bias=" << info.bias() <<
+              ", drive=" << info.drive() <<
+              ", edge_detection=" << info.edge_detection() <<
+              ", event_clock=" << info.event_clock() <<
+              ", debounced=" << ::std::boolalpha << info.debounced();
+
+       if (info.debounced())
+               out << ", debounce_period=" << info.debounce_period().count() << "us";
+
+       out << ")";
+
+       return out;
+}
+
+} /* namespace gpiod */
diff --git a/bindings/cxx/line-request.cpp b/bindings/cxx/line-request.cpp
new file mode 100644 (file)
index 0000000..f6b0a66
--- /dev/null
@@ -0,0 +1,241 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// SPDX-FileCopyrightText: 2021-2022 Bartosz Golaszewski <brgl@bgdev.pl>
+
+#include <iterator>
+#include <ostream>
+#include <utility>
+
+#include "internal.hpp"
+
+namespace gpiod {
+
+void line_request::impl::throw_if_released() const
+{
+       if (!this->request)
+               throw request_released("GPIO lines have been released");
+}
+
+void line_request::impl::set_request_ptr(line_request_ptr& ptr)
+{
+       this->request = ::std::move(ptr);
+       this->offset_buf.resize(::gpiod_line_request_get_num_requested_lines(this->request.get()));
+}
+
+void line_request::impl::fill_offset_buf(const line::offsets& offsets)
+{
+       for (unsigned int i = 0; i < offsets.size(); i++)
+               this->offset_buf[i] = offsets[i];
+}
+
+line_request::line_request()
+       : _m_priv(new impl)
+{
+
+}
+
+GPIOD_CXX_API line_request::line_request(line_request&& other) noexcept
+       : _m_priv(::std::move(other._m_priv))
+{
+
+}
+
+GPIOD_CXX_API line_request::~line_request()
+{
+
+}
+
+GPIOD_CXX_API line_request& line_request::operator=(line_request&& other) noexcept
+{
+       this->_m_priv = ::std::move(other._m_priv);
+
+       return *this;
+}
+
+GPIOD_CXX_API line_request::operator bool() const noexcept
+{
+       return this->_m_priv->request.get() != nullptr;
+}
+
+GPIOD_CXX_API void line_request::release()
+{
+       this->_m_priv->throw_if_released();
+
+       this->_m_priv->request.reset();
+}
+
+GPIOD_CXX_API ::std::string line_request::chip_name() const
+{
+       this->_m_priv->throw_if_released();
+
+       return ::gpiod_line_request_get_chip_name(this->_m_priv->request.get());
+}
+
+GPIOD_CXX_API ::std::size_t line_request::num_lines() const
+{
+       this->_m_priv->throw_if_released();
+
+       return ::gpiod_line_request_get_num_requested_lines(this->_m_priv->request.get());
+}
+
+GPIOD_CXX_API line::offsets line_request::offsets() const
+{
+       this->_m_priv->throw_if_released();
+
+       auto num_lines = this->num_lines();
+       ::std::vector<unsigned int> buf(num_lines);
+       line::offsets offsets(num_lines);
+
+       ::gpiod_line_request_get_requested_offsets(this->_m_priv->request.get(), buf.data(), buf.size());
+
+       for (unsigned int i = 0; i < num_lines; i++)
+               offsets[i] = buf[i];
+
+       return offsets;
+}
+
+GPIOD_CXX_API line::value line_request::get_value(line::offset offset)
+{
+       return this->get_values({ offset }).front();
+}
+
+GPIOD_CXX_API line::values
+line_request::get_values(const line::offsets& offsets)
+{
+       line::values vals(offsets.size());
+
+       this->get_values(offsets, vals);
+
+       return vals;
+}
+
+GPIOD_CXX_API line::values line_request::get_values()
+{
+       return this->get_values(this->offsets());
+}
+
+GPIOD_CXX_API void line_request::get_values(const line::offsets& offsets, line::values& values)
+{
+       this->_m_priv->throw_if_released();
+
+       if (offsets.size() != values.size())
+               throw ::std::invalid_argument("values must have the same size as the offsets");
+
+       this->_m_priv->fill_offset_buf(offsets);
+
+       int ret = ::gpiod_line_request_get_values_subset(
+                                       this->_m_priv->request.get(),
+                                       offsets.size(), this->_m_priv->offset_buf.data(),
+                                       reinterpret_cast<::gpiod_line_value*>(values.data()));
+       if (ret)
+               throw_from_errno("unable to retrieve line values");
+}
+
+GPIOD_CXX_API void line_request::get_values(line::values& values)
+{
+       this->get_values(this->offsets(), values);
+}
+
+GPIOD_CXX_API line_request&
+line_request::line_request::set_value(line::offset offset, line::value value)
+{
+       return this->set_values({ offset }, { value });
+}
+
+GPIOD_CXX_API line_request&
+line_request::set_values(const line::value_mappings& values)
+{
+       line::offsets offsets(values.size());
+       line::values vals(values.size());
+
+       for (unsigned int i = 0; i < values.size(); i++) {
+               offsets[i] = values[i].first;
+               vals[i] = values[i].second;
+       }
+
+       return this->set_values(offsets, vals);
+}
+
+GPIOD_CXX_API line_request& line_request::set_values(const line::offsets& offsets,
+                                           const line::values& values)
+{
+       this->_m_priv->throw_if_released();
+
+       if (offsets.size() != values.size())
+               throw ::std::invalid_argument("values must have the same size as the offsets");
+
+       this->_m_priv->fill_offset_buf(offsets);
+
+       int ret = ::gpiod_line_request_set_values_subset(
+                                       this->_m_priv->request.get(),
+                                       offsets.size(), this->_m_priv->offset_buf.data(),
+                                       reinterpret_cast<const ::gpiod_line_value*>(values.data()));
+       if (ret)
+               throw_from_errno("unable to set line values");
+
+       return *this;
+}
+
+GPIOD_CXX_API line_request& line_request::set_values(const line::values& values)
+{
+       return this->set_values(this->offsets(), values);
+}
+
+GPIOD_CXX_API line_request& line_request::reconfigure_lines(const line_config& config)
+{
+       this->_m_priv->throw_if_released();
+
+       int ret = ::gpiod_line_request_reconfigure_lines(this->_m_priv->request.get(),
+                                                        config._m_priv->config.get());
+       if (ret)
+               throw_from_errno("unable to reconfigure GPIO lines");
+
+       return *this;
+}
+
+GPIOD_CXX_API int line_request::fd() const
+{
+       this->_m_priv->throw_if_released();
+
+       return ::gpiod_line_request_get_fd(this->_m_priv->request.get());
+}
+
+GPIOD_CXX_API bool line_request::wait_edge_events(const ::std::chrono::nanoseconds& timeout) const
+{
+       this->_m_priv->throw_if_released();
+
+       int ret = ::gpiod_line_request_wait_edge_events(this->_m_priv->request.get(),
+                                                      timeout.count());
+       if (ret < 0)
+               throw_from_errno("error waiting for edge events");
+
+       return ret;
+}
+
+GPIOD_CXX_API ::std::size_t line_request::read_edge_events(edge_event_buffer& buffer)
+{
+       return this->read_edge_events(buffer, buffer.capacity());
+}
+
+GPIOD_CXX_API ::std::size_t
+line_request::read_edge_events(edge_event_buffer& buffer, ::std::size_t max_events)
+{
+       this->_m_priv->throw_if_released();
+
+       return buffer._m_priv->read_events(this->_m_priv->request, max_events);
+}
+
+GPIOD_CXX_API ::std::ostream& operator<<(::std::ostream& out, const line_request& request)
+{
+       if (!request)
+               out << "gpiod::line_request(released)";
+       else
+               out << "gpiod::line_request(chip=\"" << request.chip_name() <<
+                      "\", num_lines=" << request.num_lines() <<
+                      ", line_offsets=" << request.offsets() <<
+                      ", fd=" << request.fd() <<
+                      ")";
+
+       return out;
+}
+
+} /* namespace gpiod */
diff --git a/bindings/cxx/line-settings.cpp b/bindings/cxx/line-settings.cpp
new file mode 100644 (file)
index 0000000..300858a
--- /dev/null
@@ -0,0 +1,330 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// SPDX-FileCopyrightText: 2022 Bartosz Golaszewski <brgl@bgdev.pl>
+
+#include <map>
+#include <ostream>
+
+#include "internal.hpp"
+
+namespace gpiod {
+
+namespace {
+
+template<class cxx_enum_type, class c_enum_type>
+::std::map<c_enum_type, cxx_enum_type>
+make_reverse_maping(const ::std::map<cxx_enum_type, c_enum_type>& mapping)
+{
+       ::std::map<c_enum_type, cxx_enum_type> ret;
+
+       for (const auto &item: mapping)
+               ret[item.second] = item.first;
+
+       return ret;
+}
+
+const ::std::map<line::direction, ::gpiod_line_direction> direction_mapping = {
+       { line::direction::AS_IS,       GPIOD_LINE_DIRECTION_AS_IS },
+       { line::direction::INPUT,       GPIOD_LINE_DIRECTION_INPUT },
+       { line::direction::OUTPUT,      GPIOD_LINE_DIRECTION_OUTPUT },
+};
+
+const ::std::map<::gpiod_line_direction, line::direction>
+reverse_direction_mapping = make_reverse_maping(direction_mapping);
+
+const ::std::map<line::edge, ::gpiod_line_edge> edge_mapping = {
+       { line::edge::NONE,             GPIOD_LINE_EDGE_NONE },
+       { line::edge::FALLING,          GPIOD_LINE_EDGE_FALLING },
+       { line::edge::RISING,           GPIOD_LINE_EDGE_RISING },
+       { line::edge::BOTH,             GPIOD_LINE_EDGE_BOTH },
+};
+
+const ::std::map<::gpiod_line_edge, line::edge>
+reverse_edge_mapping = make_reverse_maping(edge_mapping);
+
+const ::std::map<line::bias, ::gpiod_line_bias> bias_mapping = {
+       { line::bias::AS_IS,            GPIOD_LINE_BIAS_AS_IS },
+       { line::bias::DISABLED,         GPIOD_LINE_BIAS_DISABLED },
+       { line::bias::PULL_UP,          GPIOD_LINE_BIAS_PULL_UP },
+       { line::bias::PULL_DOWN,        GPIOD_LINE_BIAS_PULL_DOWN },
+};
+
+const ::std::map<::gpiod_line_bias, line::bias>
+reverse_bias_mapping = make_reverse_maping(bias_mapping);
+
+const ::std::map<line::drive, ::gpiod_line_drive> drive_mapping = {
+       { line::drive::PUSH_PULL,       GPIOD_LINE_DRIVE_PUSH_PULL },
+       { line::drive::OPEN_DRAIN,      GPIOD_LINE_DRIVE_OPEN_DRAIN },
+       { line::drive::OPEN_SOURCE,     GPIOD_LINE_DRIVE_OPEN_SOURCE },
+};
+
+const ::std::map<::gpiod_line_drive, line::drive>
+reverse_drive_mapping = make_reverse_maping(drive_mapping);
+
+const ::std::map<line::clock, ::gpiod_line_clock> clock_mapping = {
+       { line::clock::MONOTONIC,       GPIOD_LINE_CLOCK_MONOTONIC },
+       { line::clock::REALTIME,        GPIOD_LINE_CLOCK_REALTIME },
+       { line::clock::HTE,             GPIOD_LINE_CLOCK_HTE },
+};
+
+const ::std::map<::gpiod_line_clock, line::clock>
+reverse_clock_mapping = make_reverse_maping(clock_mapping);
+
+const ::std::map<line::value, ::gpiod_line_value> value_mapping = {
+       { line::value::INACTIVE,        GPIOD_LINE_VALUE_INACTIVE },
+       { line::value::ACTIVE,          GPIOD_LINE_VALUE_ACTIVE },
+};
+
+const ::std::map<::gpiod_line_value, line::value>
+reverse_value_mapping = make_reverse_maping(value_mapping);
+
+line_settings_ptr make_line_settings()
+{
+       line_settings_ptr settings(::gpiod_line_settings_new());
+       if (!settings)
+               throw_from_errno("Unable to allocate the line settings object");
+
+       return settings;
+}
+
+line_settings_ptr copy_line_settings(const line_settings_ptr& ptr)
+{
+       line_settings_ptr settings(::gpiod_line_settings_copy(ptr.get()));
+       if (!settings)
+               throw_from_errno("Unable to copy the line settings object");
+
+       return settings;
+}
+
+template<class cxx_enum_type, class c_enum_type>
+c_enum_type do_map_value(cxx_enum_type value, const ::std::map<cxx_enum_type, c_enum_type>& mapping)
+try {
+       return get_mapped_value(value, mapping);
+} catch (const bad_mapping& ex) {
+       throw ::std::invalid_argument(ex.what());
+}
+
+template<class cxx_enum_type, class c_enum_type, int set_func(::gpiod_line_settings*, c_enum_type)>
+void set_mapped_prop(::gpiod_line_settings* settings, cxx_enum_type value,
+                    const ::std::map<cxx_enum_type, c_enum_type>& mapping)
+{
+       c_enum_type mapped_val = do_map_value(value, mapping);
+
+       auto ret = set_func(settings, mapped_val);
+       if (ret)
+               throw_from_errno("unable to set property");
+}
+
+template<class cxx_enum_type, class c_enum_type, c_enum_type get_func(::gpiod_line_settings*)>
+cxx_enum_type get_mapped_prop(::gpiod_line_settings* settings,
+                             const ::std::map<c_enum_type, cxx_enum_type>& mapping)
+{
+       auto mapped_val = get_func(settings);
+
+       return get_mapped_value(mapped_val, mapping);
+}
+
+} /* namespace */
+
+::gpiod_line_value map_output_value(line::value value)
+{
+       return do_map_value(value, value_mapping);
+}
+
+line_settings::impl::impl()
+       : settings(make_line_settings())
+{
+
+}
+
+line_settings::impl::impl(const impl& other)
+       : settings(copy_line_settings(other.settings))
+{
+
+}
+
+GPIOD_CXX_API line_settings::line_settings()
+       : _m_priv(new impl)
+{
+
+}
+
+GPIOD_CXX_API line_settings::line_settings(const line_settings& other)
+       : _m_priv(new impl(*other._m_priv))
+{
+
+}
+
+GPIOD_CXX_API line_settings::line_settings(line_settings&& other) noexcept
+       : _m_priv(::std::move(other._m_priv))
+{
+
+}
+
+GPIOD_CXX_API line_settings::~line_settings()
+{
+
+}
+
+GPIOD_CXX_API line_settings& line_settings::operator=(const line_settings& other)
+{
+       this->_m_priv.reset(new impl(*other._m_priv));
+
+       return *this;
+}
+
+GPIOD_CXX_API line_settings& line_settings::operator=(line_settings&& other)
+{
+       this->_m_priv = ::std::move(other._m_priv);
+
+       return *this;
+}
+
+GPIOD_CXX_API line_settings& line_settings::reset() noexcept
+{
+       ::gpiod_line_settings_reset(this->_m_priv->settings.get());
+
+       return *this;
+}
+
+GPIOD_CXX_API line_settings& line_settings::set_direction(line::direction direction)
+{
+       set_mapped_prop<line::direction, ::gpiod_line_direction,
+                       ::gpiod_line_settings_set_direction>(this->_m_priv->settings.get(),
+                                                            direction, direction_mapping);
+
+       return *this;
+}
+
+GPIOD_CXX_API line::direction line_settings::direction() const
+{
+       return get_mapped_prop<line::direction, ::gpiod_line_direction,
+                              ::gpiod_line_settings_get_direction>(
+                                                       this->_m_priv->settings.get(),
+                                                       reverse_direction_mapping);
+}
+
+GPIOD_CXX_API line_settings& line_settings::set_edge_detection(line::edge edge)
+{
+       set_mapped_prop<line::edge, ::gpiod_line_edge,
+                       ::gpiod_line_settings_set_edge_detection>(this->_m_priv->settings.get(),
+                                                                 edge, edge_mapping);
+
+       return *this;
+}
+
+GPIOD_CXX_API line::edge line_settings::edge_detection() const
+{
+       return get_mapped_prop<line::edge, ::gpiod_line_edge,
+                              ::gpiod_line_settings_get_edge_detection>(
+                                                       this->_m_priv->settings.get(),
+                                                       reverse_edge_mapping);
+}
+
+GPIOD_CXX_API line_settings& line_settings::set_bias(line::bias bias)
+{
+       set_mapped_prop<line::bias, ::gpiod_line_bias,
+                       ::gpiod_line_settings_set_bias>(this->_m_priv->settings.get(),
+                                                       bias, bias_mapping);
+
+       return *this;
+}
+
+GPIOD_CXX_API line::bias line_settings::bias() const
+{
+       return get_mapped_prop<line::bias, ::gpiod_line_bias,
+                              ::gpiod_line_settings_get_bias>(this->_m_priv->settings.get(),
+                                                               reverse_bias_mapping);
+}
+
+GPIOD_CXX_API line_settings& line_settings::set_drive(line::drive drive)
+{
+       set_mapped_prop<line::drive, ::gpiod_line_drive,
+                       ::gpiod_line_settings_set_drive>(this->_m_priv->settings.get(),
+                                                        drive, drive_mapping);
+
+       return *this;
+}
+
+GPIOD_CXX_API line::drive line_settings::drive() const
+{
+       return get_mapped_prop<line::drive, ::gpiod_line_drive,
+                              ::gpiod_line_settings_get_drive>(this->_m_priv->settings.get(),
+                                                               reverse_drive_mapping);
+}
+
+GPIOD_CXX_API line_settings& line_settings::set_active_low(bool active_low)
+{
+       ::gpiod_line_settings_set_active_low(this->_m_priv->settings.get(), active_low);
+
+       return *this;
+}
+
+GPIOD_CXX_API bool line_settings::active_low() const noexcept
+{
+       return ::gpiod_line_settings_get_active_low(this->_m_priv->settings.get());
+}
+
+GPIOD_CXX_API line_settings&
+line_settings::set_debounce_period(const ::std::chrono::microseconds& period)
+{
+       ::gpiod_line_settings_set_debounce_period_us(this->_m_priv->settings.get(), period.count());
+
+       return *this;
+}
+
+GPIOD_CXX_API ::std::chrono::microseconds line_settings::debounce_period() const noexcept
+{
+       return ::std::chrono::microseconds(
+                       ::gpiod_line_settings_get_debounce_period_us(this->_m_priv->settings.get()));
+}
+
+GPIOD_CXX_API line_settings& line_settings::set_event_clock(line::clock event_clock)
+{
+       set_mapped_prop<line::clock, ::gpiod_line_clock,
+                       ::gpiod_line_settings_set_event_clock>(this->_m_priv->settings.get(),
+                                                              event_clock, clock_mapping);
+
+       return *this;
+}
+
+GPIOD_CXX_API line::clock line_settings::event_clock() const
+{
+       return get_mapped_prop<line::clock, ::gpiod_line_clock,
+                              ::gpiod_line_settings_get_event_clock>(
+                                                       this->_m_priv->settings.get(),
+                                                       reverse_clock_mapping);
+}
+
+GPIOD_CXX_API line_settings& line_settings::set_output_value(line::value value)
+{
+       set_mapped_prop<line::value, ::gpiod_line_value,
+                       ::gpiod_line_settings_set_output_value>(this->_m_priv->settings.get(),
+                                                               value, value_mapping);
+
+       return *this;
+}
+
+GPIOD_CXX_API line::value line_settings::output_value() const
+{
+       return get_mapped_prop<line::value, ::gpiod_line_value,
+                              ::gpiod_line_settings_get_output_value>(
+                                                       this->_m_priv->settings.get(),
+                                                       reverse_value_mapping);
+}
+
+GPIOD_CXX_API ::std::ostream& operator<<(::std::ostream& out, const line_settings& settings)
+{
+       out << "gpiod::line_settings(direction=" << settings.direction() <<
+              ", edge_detection=" << settings.edge_detection() <<
+              ", bias=" << settings.bias() <<
+              ", drive=" << settings.drive() <<
+              ", " << (settings.active_low() ? "active-low" : "active-high") <<
+              ", debounce_period=" << settings.debounce_period().count() <<
+              ", event_clock=" << settings.event_clock() <<
+              ", output_value=" << settings.output_value() <<
+              ")";
+
+       return out;
+}
+
+} /* namespace gpiod */
diff --git a/bindings/cxx/line.cpp b/bindings/cxx/line.cpp
new file mode 100644 (file)
index 0000000..d6a92c1
--- /dev/null
@@ -0,0 +1,132 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// SPDX-FileCopyrightText: 2021-2022 Bartosz Golaszewski <brgl@bgdev.pl>
+
+#include <iterator>
+#include <ostream>
+
+#include "internal.hpp"
+
+namespace gpiod {
+namespace line {
+
+namespace {
+
+const ::std::map<line::value, ::std::string> value_names = {
+       { line::value::INACTIVE,        "INACTIVE" },
+       { line::value::ACTIVE,          "ACTIVE" },
+};
+
+const ::std::map<line::direction, ::std::string> direction_names = {
+       { line::direction::AS_IS,       "AS_IS" },
+       { line::direction::INPUT,       "INPUT" },
+       { line::direction::OUTPUT,      "OUTPUT" },
+};
+
+const ::std::map<line::bias, ::std::string> bias_names = {
+       { line::bias::AS_IS,            "AS_IS" },
+       { line::bias::UNKNOWN,          "UNKNOWN" },
+       { line::bias::DISABLED,         "DISABLED" },
+       { line::bias::PULL_UP,          "PULL_UP" },
+       { line::bias::PULL_DOWN,        "PULL_DOWN" },
+};
+
+const ::std::map<line::drive, ::std::string> drive_names = {
+       { line::drive::PUSH_PULL,       "PUSH_PULL" },
+       { line::drive::OPEN_DRAIN,      "OPEN_DRAIN" },
+       { line::drive::OPEN_SOURCE,     "OPEN_SOURCE" },
+};
+
+const ::std::map<line::edge, ::std::string> edge_names = {
+       { line::edge::NONE,             "NONE" },
+       { line::edge::RISING,           "RISING_EDGE" },
+       { line::edge::FALLING,          "FALLING_EDGE" },
+       { line::edge::BOTH,             "BOTH_EDGES" },
+};
+
+const ::std::map<line::clock, ::std::string> clock_names = {
+       { line::clock::MONOTONIC,       "MONOTONIC" },
+       { line::clock::REALTIME,        "REALTIME" },
+       { line::clock::HTE,             "HTE" },
+};
+
+} /* namespace */
+
+GPIOD_CXX_API ::std::ostream& operator<<(::std::ostream& out, line::value val)
+{
+       out << value_names.at(val);
+
+       return out;
+}
+
+GPIOD_CXX_API ::std::ostream& operator<<(::std::ostream& out, line::direction dir)
+{
+       out << direction_names.at(dir);
+
+       return out;
+}
+
+GPIOD_CXX_API ::std::ostream& operator<<(::std::ostream& out, line::edge edge)
+{
+       out << edge_names.at(edge);
+
+       return out;
+}
+
+GPIOD_CXX_API ::std::ostream& operator<<(::std::ostream& out, line::bias bias)
+{
+       out << bias_names.at(bias);
+
+       return out;
+}
+
+GPIOD_CXX_API ::std::ostream& operator<<(::std::ostream& out, line::drive drive)
+{
+       out << drive_names.at(drive);
+
+       return out;
+}
+
+GPIOD_CXX_API ::std::ostream& operator<<(::std::ostream& out, line::clock clock)
+{
+       out << clock_names.at(clock);
+
+       return out;
+}
+
+template<typename T>
+::std::ostream& insert_vector(::std::ostream& out,
+                             const ::std::string& name, const ::std::vector<T>& vec)
+{
+       out << name << "(";
+       ::std::copy(vec.begin(), ::std::prev(vec.end()),
+                   ::std::ostream_iterator<T>(out, ", "));
+       out << vec.back();
+       out << ")";
+
+       return out;
+}
+
+GPIOD_CXX_API ::std::ostream& operator<<(::std::ostream& out, const offsets& offs)
+{
+       return insert_vector(out, "gpiod::offsets", offs);
+}
+
+GPIOD_CXX_API ::std::ostream& operator<<(::std::ostream& out, const line::values& vals)
+{
+       return insert_vector(out, "gpiod::values", vals);
+}
+
+GPIOD_CXX_API ::std::ostream& operator<<(::std::ostream& out, const line::value_mapping& mapping)
+{
+       out << "gpiod::value_mapping(" << mapping.first << ": " << mapping.second << ")";
+
+       return out;
+}
+
+GPIOD_CXX_API ::std::ostream& operator<<(::std::ostream& out, const line::value_mappings& mappings)
+{
+       return insert_vector(out, "gpiod::value_mappings", mappings);
+}
+
+} /* namespace line */
+} /* namespace gpiod */
diff --git a/bindings/cxx/misc.cpp b/bindings/cxx/misc.cpp
new file mode 100644 (file)
index 0000000..eba0b46
--- /dev/null
@@ -0,0 +1,20 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// SPDX-FileCopyrightText: 2021-2022 Bartosz Golaszewski <brgl@bgdev.pl>
+
+#include "internal.hpp"
+
+namespace gpiod {
+
+GPIOD_CXX_API bool is_gpiochip_device(const ::std::filesystem::path& path)
+{
+       return ::gpiod_is_gpiochip_device(path.c_str());
+}
+
+GPIOD_CXX_API const ::std::string& api_version()
+{
+       static const ::std::string version(::gpiod_api_version());
+
+       return version;
+}
+
+} /* namespace gpiod */
diff --git a/bindings/cxx/request-builder.cpp b/bindings/cxx/request-builder.cpp
new file mode 100644 (file)
index 0000000..87ee2fe
--- /dev/null
@@ -0,0 +1,142 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// SPDX-FileCopyrightText: 2022 Bartosz Golaszewski <brgl@bgdev.pl>
+
+#include <ostream>
+#include <utility>
+
+#include "internal.hpp"
+
+namespace gpiod {
+
+struct request_builder::impl
+{
+       impl(chip& parent)
+               : line_cfg(),
+                 req_cfg(),
+                 parent(parent)
+       {
+
+       }
+
+       impl(const impl& other) = delete;
+       impl(impl&& other) = delete;
+       impl& operator=(const impl& other) = delete;
+       impl& operator=(impl&& other) = delete;
+
+       line_config line_cfg;
+       request_config req_cfg;
+       chip parent;
+};
+
+GPIOD_CXX_API request_builder::request_builder(chip& chip)
+       : _m_priv(new impl(chip))
+{
+
+}
+
+GPIOD_CXX_API request_builder::request_builder(request_builder&& other) noexcept
+       : _m_priv(::std::move(other._m_priv))
+{
+
+}
+
+GPIOD_CXX_API request_builder::~request_builder()
+{
+
+}
+
+GPIOD_CXX_API request_builder& request_builder::operator=(request_builder&& other) noexcept
+{
+       this->_m_priv = ::std::move(other._m_priv);
+
+       return *this;
+}
+
+GPIOD_CXX_API request_builder& request_builder::set_request_config(request_config& req_cfg)
+{
+       this->_m_priv->req_cfg = req_cfg;
+
+       return *this;
+}
+
+GPIOD_CXX_API const request_config& request_builder::get_request_config() const noexcept
+{
+       return this->_m_priv->req_cfg;
+}
+
+GPIOD_CXX_API request_builder&
+request_builder::set_consumer(const ::std::string& consumer) noexcept
+{
+       this->_m_priv->req_cfg.set_consumer(consumer);
+
+       return *this;
+}
+
+GPIOD_CXX_API request_builder&
+request_builder::set_event_buffer_size(::std::size_t event_buffer_size) noexcept
+{
+       this->_m_priv->req_cfg.set_event_buffer_size(event_buffer_size);
+
+       return *this;
+}
+
+GPIOD_CXX_API request_builder& request_builder::set_line_config(line_config &line_cfg)
+{
+       this->_m_priv->line_cfg = line_cfg;
+
+       return *this;
+}
+
+GPIOD_CXX_API const line_config& request_builder::get_line_config() const noexcept
+{
+       return this->_m_priv->line_cfg;
+}
+
+GPIOD_CXX_API request_builder&
+request_builder::add_line_settings(line::offset offset, const line_settings& settings)
+{
+       return this->add_line_settings(line::offsets({offset}), settings);
+}
+
+GPIOD_CXX_API request_builder&
+request_builder::add_line_settings(const line::offsets& offsets, const line_settings& settings)
+{
+       this->_m_priv->line_cfg.add_line_settings(offsets, settings);
+
+       return *this;
+}
+
+GPIOD_CXX_API request_builder&
+request_builder::set_output_values(const line::values& values)
+{
+       this->_m_priv->line_cfg.set_output_values(values);
+
+       return *this;
+}
+
+GPIOD_CXX_API line_request request_builder::do_request()
+{
+       line_request_ptr request(::gpiod_chip_request_lines(
+                                       this->_m_priv->parent._m_priv->chip.get(),
+                                       this->_m_priv->req_cfg._m_priv->config.get(),
+                                       this->_m_priv->line_cfg._m_priv->config.get()));
+       if (!request)
+               throw_from_errno("error requesting GPIO lines");
+
+       line_request ret;
+       ret._m_priv.get()->set_request_ptr(request);
+
+       return ret;
+}
+
+GPIOD_CXX_API ::std::ostream& operator<<(::std::ostream& out, const request_builder& builder)
+{
+       out << "gpiod::request_builder(request_config=" << builder._m_priv->req_cfg <<
+              ", line_config=" << builder._m_priv->line_cfg <<
+              ", parent=" << builder._m_priv->parent <<
+              ")";
+
+       return out;
+}
+
+} /* namespace gpiod */
diff --git a/bindings/cxx/request-config.cpp b/bindings/cxx/request-config.cpp
new file mode 100644 (file)
index 0000000..b44b8b6
--- /dev/null
@@ -0,0 +1,103 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// SPDX-FileCopyrightText: 2021 Bartosz Golaszewski <brgl@bgdev.pl>
+
+#include <ostream>
+#include <utility>
+
+#include "internal.hpp"
+
+namespace gpiod {
+
+namespace {
+
+request_config_ptr make_request_config()
+{
+       request_config_ptr config(::gpiod_request_config_new());
+       if (!config)
+               throw_from_errno("Unable to allocate the request config object");
+
+       return config;
+}
+
+} /* namespace */
+
+request_config::impl::impl()
+       : config(make_request_config())
+{
+
+}
+
+GPIOD_CXX_API request_config::request_config()
+       : _m_priv(new impl)
+{
+
+}
+
+GPIOD_CXX_API request_config::request_config(request_config&& other) noexcept
+       : _m_priv(::std::move(other._m_priv))
+{
+
+}
+
+GPIOD_CXX_API request_config::~request_config()
+{
+
+}
+
+request_config& request_config::operator=(const request_config& other)
+{
+       this->_m_priv = other._m_priv;
+
+       return *this;
+}
+
+GPIOD_CXX_API request_config& request_config::operator=(request_config&& other) noexcept
+{
+       this->_m_priv = ::std::move(other._m_priv);
+
+       return *this;
+}
+
+GPIOD_CXX_API request_config&
+request_config::set_consumer(const ::std::string& consumer) noexcept
+{
+       ::gpiod_request_config_set_consumer(this->_m_priv->config.get(), consumer.c_str());
+
+       return *this;
+}
+
+GPIOD_CXX_API ::std::string request_config::consumer() const noexcept
+{
+       const char* consumer = ::gpiod_request_config_get_consumer(this->_m_priv->config.get());
+
+       return consumer ?: "";
+}
+
+GPIOD_CXX_API request_config&
+request_config::set_event_buffer_size(::std::size_t event_buffer_size) noexcept
+{
+       ::gpiod_request_config_set_event_buffer_size(this->_m_priv->config.get(),
+                                                    event_buffer_size);
+
+       return *this;
+}
+
+GPIOD_CXX_API ::std::size_t request_config::event_buffer_size() const noexcept
+{
+       return ::gpiod_request_config_get_event_buffer_size(this->_m_priv->config.get());
+}
+
+GPIOD_CXX_API ::std::ostream& operator<<(::std::ostream& out, const request_config& config)
+{
+       ::std::string consumer;
+
+       consumer = config.consumer().empty() ? "N/A" : ::std::string("'") + config.consumer() + "'";
+
+       out << "gpiod::request_config(consumer=" << consumer <<
+              ", event_buffer_size=" << config.event_buffer_size() <<
+              ")";
+
+       return out;
+}
+
+} /* namespace gpiod */
diff --git a/bindings/cxx/tests/.gitignore b/bindings/cxx/tests/.gitignore
new file mode 100644 (file)
index 0000000..7990193
--- /dev/null
@@ -0,0 +1,4 @@
+# SPDX-License-Identifier: GPL-2.0-or-later
+# SPDX-FileCopyrightText: 2017-2021 Bartosz Golaszewski <bartekgola@gmail.com>
+
+gpiod-cxx-test
diff --git a/bindings/cxx/tests/Makefile.am b/bindings/cxx/tests/Makefile.am
new file mode 100644 (file)
index 0000000..fbf80a1
--- /dev/null
@@ -0,0 +1,30 @@
+# SPDX-License-Identifier: GPL-2.0-or-later
+# SPDX-FileCopyrightText: 2017-2021 Bartosz Golaszewski <bartekgola@gmail.com>
+
+AM_CXXFLAGS = -I$(top_srcdir)/bindings/cxx/ -I$(top_srcdir)/include
+AM_CXXFLAGS += -I$(top_srcdir)/tests/gpiosim/
+AM_CXXFLAGS += -Wall -Wextra -g -std=gnu++17 $(CATCH2_CFLAGS)
+AM_LDFLAGS = -pthread
+LDADD = $(top_builddir)/bindings/cxx/libgpiodcxx.la
+LDADD += $(top_builddir)/tests/gpiosim/libgpiosim.la
+
+noinst_PROGRAMS = gpiod-cxx-test
+
+gpiod_cxx_test_SOURCES = \
+       check-kernel.cpp \
+       gpiod-cxx-test-main.cpp \
+       gpiosim.cpp \
+       gpiosim.hpp \
+       helpers.cpp \
+       helpers.hpp \
+       tests-chip.cpp \
+       tests-chip-info.cpp \
+       tests-edge-event.cpp \
+       tests-info-event.cpp \
+       tests-line.cpp \
+       tests-line-config.cpp \
+       tests-line-info.cpp \
+       tests-line-request.cpp \
+       tests-line-settings.cpp \
+       tests-misc.cpp \
+       tests-request-config.cpp
diff --git a/bindings/cxx/tests/check-kernel.cpp b/bindings/cxx/tests/check-kernel.cpp
new file mode 100644 (file)
index 0000000..e10fb5d
--- /dev/null
@@ -0,0 +1,48 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+// SPDX-FileCopyrightText: 2017-2022 Bartosz Golaszewski <brgl@bgdev.pl>
+
+#include <linux/version.h>
+#include <sys/utsname.h>
+#include <system_error>
+#include <sstream>
+
+namespace {
+
+class kernel_checker
+{
+public:
+       kernel_checker(int major, int minor, int release)
+       {
+               int curr_major, curr_minor, curr_release, curr_ver, req_ver;
+               ::std::string major_str, minor_str, release_str;
+               ::utsname un;
+               int ret;
+
+               ret = ::uname(::std::addressof(un));
+               if (ret)
+                       throw ::std::system_error(errno, ::std::system_category(),
+                                                 "unable to read the kernel version");
+
+               ::std::stringstream ver_stream(::std::string(un.release));
+               ::std::getline(ver_stream, major_str, '.');
+               ::std::getline(ver_stream, minor_str, '.');
+               ::std::getline(ver_stream, release_str, '-');
+
+               curr_major = ::std::stoi(major_str, nullptr, 0);
+               curr_minor = ::std::stoi(minor_str, nullptr, 0);
+               curr_release = ::std::stoi(release_str, nullptr, 0);
+
+               curr_ver = KERNEL_VERSION(curr_major, curr_minor, curr_release);
+               req_ver = KERNEL_VERSION(major, minor, release);
+
+               if (curr_ver < req_ver)
+                       throw ::std::runtime_error("kernel release must be at least: " +
+                                                  ::std::to_string(major) + "." +
+                                                  ::std::to_string(minor) + "." +
+                                                  ::std::to_string(release));
+       }
+};
+
+kernel_checker require_kernel(5, 19, 0);
+
+} /* namespace */
diff --git a/bindings/cxx/tests/gpiod-cxx-test-main.cpp b/bindings/cxx/tests/gpiod-cxx-test-main.cpp
new file mode 100644 (file)
index 0000000..11bf8e5
--- /dev/null
@@ -0,0 +1,5 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+// SPDX-FileCopyrightText: 2017-2021 Bartosz Golaszewski <bartekgola@gmail.com>
+
+#define CATCH_CONFIG_MAIN
+#include <catch2/catch.hpp>
diff --git a/bindings/cxx/tests/gpiosim.cpp b/bindings/cxx/tests/gpiosim.cpp
new file mode 100644 (file)
index 0000000..4bda5a2
--- /dev/null
@@ -0,0 +1,277 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/* SPDX-FileCopyrightText: 2022 Bartosz Golaszewski <brgl@bgdev.pl> */
+
+#include <map>
+#include <system_error>
+#include <utility>
+
+#include "gpiosim.h"
+#include "gpiosim.hpp"
+
+namespace gpiosim {
+
+namespace {
+
+const ::std::map<chip::pull, gpiosim_pull> pull_mapping = {
+       { chip::pull::PULL_UP,          GPIOSIM_PULL_UP },
+       { chip::pull::PULL_DOWN,        GPIOSIM_PULL_DOWN },
+};
+
+const ::std::map<chip_builder::direction, gpiosim_direction> hog_dir_mapping = {
+       { chip_builder::direction::INPUT,       GPIOSIM_DIRECTION_INPUT },
+       { chip_builder::direction::OUTPUT_HIGH, GPIOSIM_DIRECTION_OUTPUT_HIGH },
+       { chip_builder::direction::OUTPUT_LOW,  GPIOSIM_DIRECTION_OUTPUT_LOW },
+};
+
+const ::std::map<gpiosim_value, chip::value> value_mapping = {
+       { GPIOSIM_VALUE_INACTIVE,       chip::value::INACTIVE },
+       { GPIOSIM_VALUE_ACTIVE,         chip::value::ACTIVE },
+};
+
+template<class gpiosim_type, void free_func(gpiosim_type*)> struct deleter
+{
+       void operator()(gpiosim_type* ptr)
+       {
+               free_func(ptr);
+       }
+};
+
+using ctx_deleter = deleter<::gpiosim_ctx, ::gpiosim_ctx_unref>;
+using dev_deleter = deleter<::gpiosim_dev, ::gpiosim_dev_unref>;
+using bank_deleter = deleter<::gpiosim_bank, ::gpiosim_bank_unref>;
+
+using ctx_ptr = ::std::unique_ptr<::gpiosim_ctx, ctx_deleter>;
+using dev_ptr = ::std::unique_ptr<::gpiosim_dev, dev_deleter>;
+using bank_ptr = ::std::unique_ptr<::gpiosim_bank, bank_deleter>;
+
+ctx_ptr sim_ctx;
+
+class sim_ctx_initializer
+{
+public:
+       sim_ctx_initializer()
+       {
+               sim_ctx.reset(gpiosim_ctx_new());
+               if (!sim_ctx)
+                       throw ::std::system_error(errno, ::std::system_category(),
+                                                 "unable to create the GPIO simulator context");
+       }
+};
+
+dev_ptr make_sim_dev()
+{
+       static sim_ctx_initializer ctx_initializer;
+
+       dev_ptr dev(::gpiosim_dev_new(sim_ctx.get()));
+       if (!dev)
+               throw ::std::system_error(errno, ::std::system_category(),
+                                         "failed to create a new GPIO simulator device");
+
+       return dev;
+}
+
+bank_ptr make_sim_bank(const dev_ptr& dev)
+{
+       bank_ptr bank(::gpiosim_bank_new(dev.get()));
+       if (!bank)
+               throw ::std::system_error(errno, ::std::system_category(),
+                                         "failed to create a new GPIO simulator bank");
+
+       return bank;
+}
+
+} /* namespace */
+
+struct chip::impl
+{
+       impl()
+               : dev(make_sim_dev()),
+                 bank(make_sim_bank(this->dev))
+       {
+
+       }
+
+       impl(const impl& other) = delete;
+       impl(impl&& other) = delete;
+       ~impl() = default;
+       impl& operator=(const impl& other) = delete;
+       impl& operator=(impl&& other) = delete;
+
+       dev_ptr dev;
+       bank_ptr bank;
+};
+
+chip::chip()
+       : _m_priv(new impl)
+{
+
+}
+
+chip::chip(chip&& other)
+       : _m_priv(::std::move(other._m_priv))
+{
+
+}
+
+chip::~chip()
+{
+
+}
+
+chip& chip::operator=(chip&& other)
+{
+       this->_m_priv = ::std::move(other._m_priv);
+
+       return *this;
+}
+
+::std::filesystem::path chip::dev_path() const
+{
+       return ::gpiosim_bank_get_dev_path(this->_m_priv->bank.get());
+}
+
+::std::string chip::name() const
+{
+       return ::gpiosim_bank_get_chip_name(this->_m_priv->bank.get());
+}
+
+chip::value chip::get_value(unsigned int offset)
+{
+       auto val = ::gpiosim_bank_get_value(this->_m_priv->bank.get(), offset);
+       if (val == GPIOSIM_VALUE_ERROR)
+               throw ::std::system_error(errno, ::std::system_category(),
+                                         "failed to read the simulated GPIO line value");
+
+       return value_mapping.at(val);
+}
+
+void chip::set_pull(unsigned int offset, pull pull)
+{
+       auto ret = ::gpiosim_bank_set_pull(this->_m_priv->bank.get(),
+                                          offset, pull_mapping.at(pull));
+       if (ret)
+               throw ::std::system_error(errno, ::std::system_category(),
+                                         "failed to set the pull of simulated GPIO line");
+}
+
+struct chip_builder::impl
+{
+       impl()
+               : num_lines(0),
+                 label(),
+                 line_names(),
+                 hogs()
+       {
+
+       }
+
+       ::std::size_t num_lines;
+       ::std::string label;
+       ::std::map<unsigned int, ::std::string> line_names;
+       ::std::map<unsigned int, ::std::pair<::std::string, direction>> hogs;
+};
+
+chip_builder::chip_builder()
+       : _m_priv(new impl)
+{
+
+}
+
+chip_builder::chip_builder(chip_builder&& other)
+       : _m_priv(::std::move(other._m_priv))
+{
+
+}
+
+chip_builder::~chip_builder()
+{
+
+}
+
+chip_builder& chip_builder::operator=(chip_builder&& other)
+{
+       this->_m_priv = ::std::move(other._m_priv);
+
+       return *this;
+}
+
+chip_builder& chip_builder::set_num_lines(::std::size_t num_lines)
+{
+       this->_m_priv->num_lines = num_lines;
+
+       return *this;
+}
+
+chip_builder& chip_builder::set_label(const ::std::string& label)
+{
+       this->_m_priv->label = label;
+
+       return *this;
+}
+
+chip_builder& chip_builder::set_line_name(unsigned int offset, const ::std::string& name)
+{
+       this->_m_priv->line_names[offset] = name;
+
+       return *this;
+}
+
+chip_builder& chip_builder::set_hog(unsigned int offset, const ::std::string& name, direction direction)
+{
+       this->_m_priv->hogs[offset] = { name, direction };
+
+       return *this;
+}
+
+chip chip_builder::build()
+{
+       chip sim;
+       int ret;
+
+       if (this->_m_priv->num_lines) {
+               ret = ::gpiosim_bank_set_num_lines(sim._m_priv->bank.get(),
+                                                  this->_m_priv->num_lines);
+               if (ret)
+                       throw ::std::system_error(errno, ::std::system_category(),
+                                                 "failed to set number of lines");
+       }
+
+       if (!this->_m_priv->label.empty()) {
+               ret = ::gpiosim_bank_set_label(sim._m_priv->bank.get(),
+                                              this->_m_priv->label.c_str());
+               if (ret)
+                       throw ::std::system_error(errno, ::std::system_category(),
+                                                 "failed to set the chip label");
+       }
+
+       for (const auto& name: this->_m_priv->line_names) {
+               ret = ::gpiosim_bank_set_line_name(sim._m_priv->bank.get(),
+                                                name.first, name.second.c_str());
+               if (ret)
+                       throw ::std::system_error(errno, ::std::system_category(),
+                                                 "failed to set the line name");
+       }
+
+       for (const auto& hog: this->_m_priv->hogs) {
+               ret = ::gpiosim_bank_hog_line(sim._m_priv->bank.get(), hog.first,
+                                             hog.second.first.c_str(),
+                                             hog_dir_mapping.at(hog.second.second));
+               if (ret)
+                       throw ::std::system_error(errno, ::std::system_category(),
+                                                 "failed to hog the line");
+       }
+
+       ret = ::gpiosim_dev_enable(sim._m_priv->dev.get());
+       if (ret)
+               throw ::std::system_error(errno, ::std::system_category(),
+                                         "failed to enable the simulated GPIO device");
+
+       return sim;
+}
+
+chip_builder make_sim()
+{
+       return chip_builder();
+}
+
+} /* namespace gpiosim */
diff --git a/bindings/cxx/tests/gpiosim.hpp b/bindings/cxx/tests/gpiosim.hpp
new file mode 100644 (file)
index 0000000..8151af6
--- /dev/null
@@ -0,0 +1,86 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/* SPDX-FileCopyrightText: 2022 Bartosz Golaszewski <brgl@bgdev.pl> */
+
+#ifndef __GPIOD_CXX_GPIOSIM_HPP__
+#define __GPIOD_CXX_GPIOSIM_HPP__
+
+#include <filesystem>
+#include <memory>
+
+namespace gpiosim {
+
+class chip_builder;
+
+class chip
+{
+public:
+       enum class pull {
+               PULL_UP = 1,
+               PULL_DOWN,
+       };
+
+       enum class value {
+               INACTIVE = 0,
+               ACTIVE = 1,
+       };
+
+       chip(const chip& other) = delete;
+       chip(chip&& other);
+       ~chip();
+
+       chip& operator=(const chip& other) = delete;
+       chip& operator=(chip&& other);
+
+       ::std::filesystem::path dev_path() const;
+       ::std::string name() const;
+
+       value get_value(unsigned int offset);
+       void set_pull(unsigned int offset, pull pull);
+
+private:
+
+       chip();
+
+       struct impl;
+
+       ::std::unique_ptr<impl> _m_priv;
+
+       friend chip_builder;
+};
+
+class chip_builder
+{
+public:
+       enum class direction {
+               INPUT = 1,
+               OUTPUT_HIGH,
+               OUTPUT_LOW,
+       };
+
+       chip_builder();
+       chip_builder(const chip_builder& other) = delete;
+       chip_builder(chip_builder&& other);
+       ~chip_builder();
+
+       chip_builder& operator=(const chip_builder& other) = delete;
+       chip_builder& operator=(chip_builder&& other);
+
+       chip_builder& set_num_lines(::std::size_t num_lines);
+       chip_builder& set_label(const ::std::string& label);
+       chip_builder& set_line_name(unsigned int offset, const ::std::string& name);
+       chip_builder& set_hog(unsigned int offset, const ::std::string& name, direction direction);
+
+       chip build();
+
+private:
+
+       struct impl;
+
+       ::std::unique_ptr<impl> _m_priv;
+};
+
+chip_builder make_sim();
+
+} /* namespace gpiosim */
+
+#endif /* __GPIOD_CXX_GPIOSIM_HPP__ */
diff --git a/bindings/cxx/tests/helpers.cpp b/bindings/cxx/tests/helpers.cpp
new file mode 100644 (file)
index 0000000..eb2c3db
--- /dev/null
@@ -0,0 +1,37 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// SPDX-FileCopyrightText: 2021-2022 Bartosz Golaszewski <brgl@bgdev.pl>
+
+#include "helpers.hpp"
+
+system_error_matcher::system_error_matcher(int expected_errno)
+       : _m_cond(::std::system_category().default_error_condition(expected_errno))
+{
+
+}
+
+::std::string system_error_matcher::describe() const
+{
+       return "matches: errno " + ::std::to_string(this->_m_cond.value());
+}
+
+bool system_error_matcher::match(const ::std::system_error& error) const
+{
+       return error.code().value() == this->_m_cond.value();
+}
+
+regex_matcher::regex_matcher(const ::std::string& pattern)
+       : _m_pattern(pattern),
+         _m_repr("matches: regex \"" + pattern + "\"")
+{
+
+}
+
+::std::string regex_matcher::describe() const
+{
+       return this->_m_repr;
+}
+
+bool regex_matcher::match(const ::std::string& str) const
+{
+       return ::std::regex_match(str, this->_m_pattern);
+}
diff --git a/bindings/cxx/tests/helpers.hpp b/bindings/cxx/tests/helpers.hpp
new file mode 100644 (file)
index 0000000..62d9827
--- /dev/null
@@ -0,0 +1,62 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/* SPDX-FileCopyrightText: 2021-2022 Bartosz Golaszewski <brgl@bgdev.pl> */
+
+#ifndef __GPIOD_CXX_TEST_HELPERS_HPP__
+#define __GPIOD_CXX_TEST_HELPERS_HPP__
+
+#include <catch2/catch.hpp>
+#include <regex>
+#include <string>
+#include <sstream>
+#include <system_error>
+
+class system_error_matcher : public Catch::MatcherBase<::std::system_error>
+{
+public:
+       explicit system_error_matcher(int expected_errno);
+       ::std::string describe() const override;
+       bool match(const ::std::system_error& error) const override;
+
+private:
+       ::std::error_condition _m_cond;
+};
+
+class regex_matcher : public Catch::MatcherBase<::std::string>
+{
+public:
+       explicit regex_matcher(const ::std::string& pattern);
+       ::std::string describe() const override;
+       bool match(const ::std::string& str) const override;
+
+private:
+       ::std::regex _m_pattern;
+       ::std::string _m_repr;
+};
+
+template<class T> class stringify_matcher : public Catch::MatcherBase<T>
+{
+public:
+       explicit stringify_matcher(const ::std::string& expected) : _m_expected(expected)
+       {
+
+       }
+
+       ::std::string describe() const override
+       {
+               return "equals " + this->_m_expected;
+       }
+
+       bool match(const T& obj) const override
+       {
+               ::std::stringstream buf;
+
+               buf << obj;
+
+               return buf.str() == this->_m_expected;
+       }
+
+private:
+       ::std::string _m_expected;
+};
+
+#endif /* __GPIOD_CXX_TEST_HELPERS_HPP__ */
diff --git a/bindings/cxx/tests/tests-chip-info.cpp b/bindings/cxx/tests/tests-chip-info.cpp
new file mode 100644 (file)
index 0000000..717c387
--- /dev/null
@@ -0,0 +1,117 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+// SPDX-FileCopyrightText: 2021-2022 Bartosz Golaszewski <brgl@bgdev.pl>
+
+#include <catch2/catch.hpp>
+#include <gpiod.hpp>
+#include <sstream>
+
+#include "gpiosim.hpp"
+#include "helpers.hpp"
+
+using ::gpiosim::make_sim;
+
+namespace {
+
+TEST_CASE("chip_info properties can be read", "[chip-info][chip]")
+{
+       auto sim = make_sim()
+               .set_num_lines(8)
+               .set_label("foobar")
+               .build();
+
+       ::gpiod::chip chip(sim.dev_path());
+       auto info = chip.get_info();
+
+       SECTION("get chip name")
+       {
+               REQUIRE_THAT(info.name(), Catch::Equals(sim.name()));
+       }
+
+       SECTION("get chip label")
+       {
+               REQUIRE_THAT(info.label(), Catch::Equals("foobar"));
+       }
+
+       SECTION("get num_lines")
+       {
+               REQUIRE(info.num_lines() == 8);
+       }
+}
+
+TEST_CASE("chip_info can be copied and moved", "[chip-info]")
+{
+       auto sim = make_sim()
+               .set_num_lines(4)
+               .set_label("foobar")
+               .build();
+
+       ::gpiod::chip chip(sim.dev_path());
+       auto info = chip.get_info();
+
+       SECTION("copy constructor works")
+       {
+               auto copy(info);
+
+               REQUIRE_THAT(copy.name(), Catch::Equals(sim.name()));
+               REQUIRE_THAT(copy.label(), Catch::Equals("foobar"));
+               REQUIRE(copy.num_lines() == 4);
+
+               REQUIRE_THAT(info.name(), Catch::Equals(sim.name()));
+               REQUIRE_THAT(info.label(), Catch::Equals("foobar"));
+               REQUIRE(info.num_lines() == 4);
+       }
+
+       SECTION("assignment operator works")
+       {
+               auto copy = chip.get_info();
+
+               copy = info;
+
+               REQUIRE_THAT(copy.name(), Catch::Equals(sim.name()));
+               REQUIRE_THAT(copy.label(), Catch::Equals("foobar"));
+               REQUIRE(copy.num_lines() == 4);
+
+               REQUIRE_THAT(info.name(), Catch::Equals(sim.name()));
+               REQUIRE_THAT(info.label(), Catch::Equals("foobar"));
+               REQUIRE(info.num_lines() == 4);
+       }
+
+       SECTION("move constructor works")
+       {
+               auto moved(std::move(info));
+
+               REQUIRE_THAT(moved.name(), Catch::Equals(sim.name()));
+               REQUIRE_THAT(moved.label(), Catch::Equals("foobar"));
+               REQUIRE(moved.num_lines() == 4);
+       }
+
+       SECTION("move assignment operator works")
+       {
+               auto moved = chip.get_info();
+
+               moved = ::std::move(info);
+
+               REQUIRE_THAT(moved.name(), Catch::Equals(sim.name()));
+               REQUIRE_THAT(moved.label(), Catch::Equals("foobar"));
+               REQUIRE(moved.num_lines() == 4);
+       }
+}
+
+TEST_CASE("stream insertion operator works for chip_info", "[chip-info]")
+{
+       auto sim = make_sim()
+               .set_num_lines(4)
+               .set_label("foobar")
+               .build();
+
+       ::gpiod::chip chip(sim.dev_path());
+       auto info = chip.get_info();
+       ::std::stringstream expected;
+
+       expected << "gpiod::chip_info(name=\"" << sim.name() <<
+                   "\", label=\"foobar\", num_lines=4)";
+
+       REQUIRE_THAT(info, stringify_matcher<::gpiod::chip_info>(expected.str()));
+}
+
+} /* namespace */
diff --git a/bindings/cxx/tests/tests-chip.cpp b/bindings/cxx/tests/tests-chip.cpp
new file mode 100644 (file)
index 0000000..c5ec19b
--- /dev/null
@@ -0,0 +1,182 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+// SPDX-FileCopyrightText: 2021-2022 Bartosz Golaszewski <brgl@bgdev.pl>
+
+#include <catch2/catch.hpp>
+#include <gpiod.hpp>
+#include <sstream>
+#include <system_error>
+#include <utility>
+
+#include "gpiosim.hpp"
+#include "helpers.hpp"
+
+using ::gpiosim::make_sim;
+
+namespace {
+
+TEST_CASE("chip constructor works", "[chip]")
+{
+       SECTION("open an existing GPIO chip")
+       {
+               auto sim = make_sim().build();
+
+               REQUIRE_NOTHROW(::gpiod::chip(sim.dev_path()));
+       }
+
+       SECTION("opening a nonexistent file fails with ENOENT")
+       {
+               REQUIRE_THROWS_MATCHES(::gpiod::chip("/dev/nonexistent"),
+                                      ::std::system_error, system_error_matcher(ENOENT));
+       }
+
+       SECTION("opening a file that is not a device fails with ENOTTY")
+       {
+               REQUIRE_THROWS_MATCHES(::gpiod::chip("/tmp"),
+                                      ::std::system_error, system_error_matcher(ENOTTY));
+       }
+
+       SECTION("opening a non-GPIO character device fails with ENODEV")
+       {
+               REQUIRE_THROWS_MATCHES(::gpiod::chip("/dev/null"),
+                                      ::std::system_error, system_error_matcher(ENODEV));
+       }
+
+       SECTION("move constructor")
+       {
+               auto sim = make_sim()
+                       .set_label("foobar")
+                       .build();
+
+               ::gpiod::chip first(sim.dev_path());
+               REQUIRE_THAT(first.get_info().label(), Catch::Equals("foobar"));
+               ::gpiod::chip second(::std::move(first));
+               REQUIRE_THAT(second.get_info().label(), Catch::Equals("foobar"));
+       }
+}
+
+TEST_CASE("chip operators work", "[chip]")
+{
+       auto sim = make_sim()
+               .set_label("foobar")
+               .build();
+
+       ::gpiod::chip chip(sim.dev_path());
+
+       SECTION("assignment operator")
+       {
+               auto moved_sim = make_sim()
+                       .set_label("moved")
+                       .build();
+
+               ::gpiod::chip moved_chip(moved_sim.dev_path());
+
+               REQUIRE_THAT(chip.get_info().label(), Catch::Equals("foobar"));
+               chip = ::std::move(moved_chip);
+               REQUIRE_THAT(chip.get_info().label(), Catch::Equals("moved"));
+       }
+
+       SECTION("boolean operator")
+       {
+               REQUIRE(chip);
+               chip.close();
+               REQUIRE_FALSE(chip);
+       }
+}
+
+TEST_CASE("chip properties can be read", "[chip]")
+{
+       auto sim = make_sim()
+               .set_num_lines(8)
+               .set_label("foobar")
+               .build();
+
+       ::gpiod::chip chip(sim.dev_path());
+
+       SECTION("get device path")
+       {
+               REQUIRE_THAT(chip.path(), Catch::Equals(sim.dev_path()));
+       }
+
+       SECTION("get file descriptor")
+       {
+               REQUIRE(chip.fd() >= 0);
+       }
+}
+
+TEST_CASE("line lookup by name works", "[chip]")
+{
+       auto sim = make_sim()
+               .set_num_lines(8)
+               .set_line_name(0, "foo")
+               .set_line_name(2, "bar")
+               .set_line_name(3, "baz")
+               .set_line_name(5, "xyz")
+               .build();
+
+       ::gpiod::chip chip(sim.dev_path());
+
+       SECTION("lookup successful")
+       {
+               REQUIRE(chip.get_line_offset_from_name("baz") == 3);
+       }
+
+       SECTION("lookup failed")
+       {
+               REQUIRE(chip.get_line_offset_from_name("nonexistent") < 0);
+       }
+}
+
+TEST_CASE("line lookup: behavior for duplicate names", "[chip]")
+{
+       auto sim = make_sim()
+               .set_num_lines(8)
+               .set_line_name(0, "foo")
+               .set_line_name(2, "bar")
+               .set_line_name(3, "baz")
+               .set_line_name(5, "bar")
+               .build();
+
+       ::gpiod::chip chip(sim.dev_path());
+
+       REQUIRE(chip.get_line_offset_from_name("bar") == 2);
+}
+
+TEST_CASE("closed chip can no longer be used", "[chip]")
+{
+       auto sim = make_sim().build();
+
+       ::gpiod::chip chip(sim.dev_path());
+       chip.close();
+       REQUIRE_THROWS_AS(chip.path(), ::gpiod::chip_closed);
+}
+
+TEST_CASE("stream insertion operator works for chip", "[chip]")
+{
+       auto sim = make_sim()
+               .set_num_lines(4)
+               .set_label("foobar")
+               .build();
+
+       ::gpiod::chip chip(sim.dev_path());
+       ::std::stringstream buf;
+
+       SECTION("open chip")
+       {
+               ::std::stringstream expected;
+
+               expected << "gpiod::chip(path=" << sim.dev_path() <<
+                           ", info=gpiod::chip_info(name=\"" << sim.name() <<
+                           "\", label=\"foobar\", num_lines=4))";
+
+               buf << chip;
+               REQUIRE_THAT(buf.str(), Catch::Equals(expected.str()));
+       }
+
+       SECTION("closed chip")
+       {
+               chip.close();
+               REQUIRE_THAT(chip, stringify_matcher<::gpiod::chip>("gpiod::chip(closed)"));
+       }
+}
+
+} /* namespace */
diff --git a/bindings/cxx/tests/tests-edge-event.cpp b/bindings/cxx/tests/tests-edge-event.cpp
new file mode 100644 (file)
index 0000000..19a6ab3
--- /dev/null
@@ -0,0 +1,422 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+// SPDX-FileCopyrightText: 2022 Bartosz Golaszewski <brgl@bgdev.pl>
+
+#include <catch2/catch.hpp>
+#include <chrono>
+#include <gpiod.hpp>
+#include <sstream>
+#include <thread>
+#include <utility>
+
+#include "gpiosim.hpp"
+#include "helpers.hpp"
+
+using ::gpiosim::make_sim;
+using direction = ::gpiod::line::direction;
+using edge = ::gpiod::line::edge;
+using offsets = ::gpiod::line::offsets;
+using pull = ::gpiosim::chip::pull;
+using event_type = ::gpiod::edge_event::event_type;
+
+namespace {
+
+TEST_CASE("edge_event_buffer capacity settings work", "[edge-event]")
+{
+       SECTION("default capacity")
+       {
+               REQUIRE(::gpiod::edge_event_buffer().capacity() == 64);
+       }
+
+       SECTION("user-defined capacity")
+       {
+               REQUIRE(::gpiod::edge_event_buffer(123).capacity() == 123);
+       }
+
+       SECTION("max capacity")
+       {
+               REQUIRE(::gpiod::edge_event_buffer(16 * 64 * 2).capacity() == 1024);
+       }
+}
+
+TEST_CASE("edge_event wait timeout", "[edge-event]")
+{
+       auto sim = make_sim().build();
+       ::gpiod::chip chip(sim.dev_path());
+
+       auto request = chip.prepare_request()
+               .add_line_settings(
+                       0,
+                       ::gpiod::line_settings()
+                               .set_edge_detection(edge::BOTH)
+               )
+               .do_request();
+
+       REQUIRE_FALSE(request.wait_edge_events(::std::chrono::milliseconds(100)));
+}
+
+TEST_CASE("output mode and edge detection don't work together", "[edge-event]")
+{
+       auto sim = make_sim().build();
+
+       REQUIRE_THROWS_AS(
+               ::gpiod::chip(sim.dev_path())
+                       .prepare_request()
+                       .add_line_settings(
+                               0,
+                               ::gpiod::line_settings()
+                                       .set_direction(direction::OUTPUT)
+                                       .set_edge_detection(edge::BOTH)
+                       )
+                       .do_request(),
+               ::std::invalid_argument
+       );
+}
+
+void trigger_falling_and_rising_edge(::gpiosim::chip& sim, unsigned int offset)
+{
+       ::std::this_thread::sleep_for(::std::chrono::milliseconds(30));
+       sim.set_pull(offset, pull::PULL_UP);
+       ::std::this_thread::sleep_for(::std::chrono::milliseconds(30));
+       sim.set_pull(offset, pull::PULL_DOWN);
+}
+
+void trigger_rising_edge_events_on_two_offsets(::gpiosim::chip& sim,
+                                              unsigned int off0, unsigned int off1)
+{
+       ::std::this_thread::sleep_for(::std::chrono::milliseconds(30));
+       sim.set_pull(off0, pull::PULL_UP);
+       ::std::this_thread::sleep_for(::std::chrono::milliseconds(30));
+       sim.set_pull(off1, pull::PULL_UP);
+}
+
+TEST_CASE("waiting for and reading edge events works", "[edge-event]")
+{
+       auto sim = make_sim()
+               .set_num_lines(8)
+               .build();
+
+       ::gpiod::chip chip(sim.dev_path());
+       ::gpiod::edge_event_buffer buffer;
+
+       SECTION("both edge events")
+       {
+               auto request = chip
+                       .prepare_request()
+                       .add_line_settings(
+                               2,
+                               ::gpiod::line_settings()
+                                       .set_edge_detection(edge::BOTH)
+                       )
+                       .do_request();
+
+               ::std::uint64_t ts_rising, ts_falling;
+
+               ::std::thread thread(trigger_falling_and_rising_edge, ::std::ref(sim), 2);
+
+               REQUIRE(request.wait_edge_events(::std::chrono::seconds(1)));
+               REQUIRE(request.read_edge_events(buffer, 1) == 1);
+               REQUIRE(buffer.num_events() == 1);
+               auto event = buffer.get_event(0);
+               REQUIRE(event.type() == event_type::RISING_EDGE);
+               REQUIRE(event.line_offset() == 2);
+               ts_rising = event.timestamp_ns();
+
+               REQUIRE(request.wait_edge_events(::std::chrono::seconds(1)));
+               REQUIRE(request.read_edge_events(buffer, 1) == 1);
+               REQUIRE(buffer.num_events() == 1);
+               event = buffer.get_event(0);
+               REQUIRE(event.type() == event_type::FALLING_EDGE);
+               REQUIRE(event.line_offset() == 2);
+               ts_falling = event.timestamp_ns();
+
+               REQUIRE_FALSE(request.wait_edge_events(::std::chrono::milliseconds(100)));
+
+               thread.join();
+
+               REQUIRE(ts_falling > ts_rising);
+       }
+
+       SECTION("rising edge event")
+       {
+               auto request = chip
+                       .prepare_request()
+                       .add_line_settings(
+                               6,
+                               ::gpiod::line_settings()
+                                       .set_edge_detection(edge::RISING)
+                       )
+                       .do_request();
+
+               ::std::thread thread(trigger_falling_and_rising_edge, ::std::ref(sim), 6);
+
+               REQUIRE(request.wait_edge_events(::std::chrono::seconds(1)));
+               REQUIRE(request.read_edge_events(buffer, 1) == 1);
+               REQUIRE(buffer.num_events() == 1);
+               auto event = buffer.get_event(0);
+               REQUIRE(event.type() == event_type::RISING_EDGE);
+               REQUIRE(event.line_offset() == 6);
+
+               REQUIRE_FALSE(request.wait_edge_events(::std::chrono::milliseconds(100)));
+
+               thread.join();
+       }
+
+       SECTION("falling edge event")
+       {
+               auto request = chip
+                       .prepare_request()
+                       .add_line_settings(
+                               7,
+                               ::gpiod::line_settings()
+                                       .set_edge_detection(edge::FALLING)
+                       )
+                       .do_request();
+
+               ::std::thread thread(trigger_falling_and_rising_edge, ::std::ref(sim), 7);
+
+               REQUIRE(request.wait_edge_events(::std::chrono::seconds(1)));
+               REQUIRE(request.read_edge_events(buffer, 1) == 1);
+               REQUIRE(buffer.num_events() == 1);
+               auto event = buffer.get_event(0);
+               REQUIRE(event.type() == event_type::FALLING_EDGE);
+               REQUIRE(event.line_offset() == 7);
+
+               REQUIRE_FALSE(request.wait_edge_events(::std::chrono::milliseconds(100)));
+
+               thread.join();
+       }
+
+       SECTION("sequence numbers")
+       {
+               auto request = chip
+                       .prepare_request()
+                       .add_line_settings(
+                               { 0, 1 },
+                               ::gpiod::line_settings()
+                                       .set_edge_detection(edge::BOTH)
+                       )
+                       .do_request();
+
+               ::std::thread thread(trigger_rising_edge_events_on_two_offsets, ::std::ref(sim), 0, 1);
+
+               REQUIRE(request.wait_edge_events(::std::chrono::seconds(1)));
+               REQUIRE(request.read_edge_events(buffer, 1) == 1);
+               REQUIRE(buffer.num_events() == 1);
+               auto event = buffer.get_event(0);
+               REQUIRE(event.type() == event_type::RISING_EDGE);
+               REQUIRE(event.line_offset() == 0);
+               REQUIRE(event.global_seqno() == 1);
+               REQUIRE(event.line_seqno() == 1);
+
+               REQUIRE(request.wait_edge_events(::std::chrono::seconds(1)));
+               REQUIRE(request.read_edge_events(buffer, 1) == 1);
+               REQUIRE(buffer.num_events() == 1);
+               event = buffer.get_event(0);
+               REQUIRE(event.type() == event_type::RISING_EDGE);
+               REQUIRE(event.line_offset() == 1);
+               REQUIRE(event.global_seqno() == 2);
+               REQUIRE(event.line_seqno() == 1);
+
+               thread.join();
+       }
+}
+
+TEST_CASE("reading multiple events", "[edge-event]")
+{
+       auto sim = make_sim()
+               .set_num_lines(8)
+               .build();
+
+       ::gpiod::chip chip(sim.dev_path());
+
+       auto request = chip
+               .prepare_request()
+               .add_line_settings(
+                       1,
+                       ::gpiod::line_settings()
+                               .set_edge_detection(edge::BOTH)
+               )
+               .do_request();
+
+       unsigned long line_seqno = 1, global_seqno = 1;
+
+       sim.set_pull(1, pull::PULL_UP);
+       ::std::this_thread::sleep_for(::std::chrono::milliseconds(10));
+       sim.set_pull(1, pull::PULL_DOWN);
+       ::std::this_thread::sleep_for(::std::chrono::milliseconds(10));
+       sim.set_pull(1, pull::PULL_UP);
+       ::std::this_thread::sleep_for(::std::chrono::milliseconds(10));
+
+       SECTION("read multiple events")
+       {
+               ::gpiod::edge_event_buffer buffer;
+
+               REQUIRE(request.wait_edge_events(::std::chrono::seconds(1)));
+               REQUIRE(request.read_edge_events(buffer) == 3);
+               REQUIRE(buffer.num_events() == 3);
+
+               for (const auto& event: buffer) {
+                       REQUIRE(event.line_offset() == 1);
+                       REQUIRE(event.line_seqno() == line_seqno++);
+                       REQUIRE(event.global_seqno() == global_seqno++);
+               }
+       }
+
+       SECTION("read over capacity")
+       {
+               ::gpiod::edge_event_buffer buffer(2);
+
+               REQUIRE(request.wait_edge_events(::std::chrono::seconds(1)));
+               REQUIRE(request.read_edge_events(buffer) == 2);
+               REQUIRE(buffer.num_events() == 2);
+       }
+}
+
+TEST_CASE("edge_event_buffer can be moved", "[edge-event]")
+{
+       auto sim = make_sim()
+               .set_num_lines(2)
+               .build();
+
+       ::gpiod::chip chip(sim.dev_path());
+       ::gpiod::edge_event_buffer buffer(13);
+
+       /* Get some events into the buffer. */
+       auto request = chip
+               .prepare_request()
+               .add_line_settings(
+                       1,
+                       ::gpiod::line_settings()
+                               .set_edge_detection(edge::BOTH)
+               )
+               .do_request();
+
+       sim.set_pull(1, pull::PULL_UP);
+       ::std::this_thread::sleep_for(::std::chrono::milliseconds(10));
+       sim.set_pull(1, pull::PULL_DOWN);
+       ::std::this_thread::sleep_for(::std::chrono::milliseconds(10));
+       sim.set_pull(1, pull::PULL_UP);
+       ::std::this_thread::sleep_for(::std::chrono::milliseconds(10));
+
+       ::std::this_thread::sleep_for(::std::chrono::milliseconds(500));
+
+       REQUIRE(request.wait_edge_events(::std::chrono::seconds(1)));
+       REQUIRE(request.read_edge_events(buffer) == 3);
+
+       SECTION("move constructor works")
+       {
+               auto moved(::std::move(buffer));
+               REQUIRE(moved.capacity() == 13);
+               REQUIRE(moved.num_events() == 3);
+       }
+
+       SECTION("move assignment operator works")
+       {
+               ::gpiod::edge_event_buffer moved;
+
+               moved = ::std::move(buffer);
+               REQUIRE(moved.capacity() == 13);
+               REQUIRE(moved.num_events() == 3);
+       }
+}
+
+TEST_CASE("edge_event can be copied and moved", "[edge-event]")
+{
+       auto sim = make_sim().build();
+       ::gpiod::chip chip(sim.dev_path());
+       ::gpiod::edge_event_buffer buffer;
+
+       auto request = chip
+               .prepare_request()
+               .add_line_settings(
+                       0,
+                       ::gpiod::line_settings()
+                               .set_edge_detection(edge::BOTH)
+               )
+               .do_request();
+
+       sim.set_pull(0, pull::PULL_UP);
+       ::std::this_thread::sleep_for(::std::chrono::milliseconds(10));
+       REQUIRE(request.wait_edge_events(::std::chrono::seconds(1)));
+       REQUIRE(request.read_edge_events(buffer) == 1);
+       auto event = buffer.get_event(0);
+
+       sim.set_pull(0, pull::PULL_DOWN);
+       ::std::this_thread::sleep_for(::std::chrono::milliseconds(10));
+       REQUIRE(request.wait_edge_events(::std::chrono::seconds(1)));
+       REQUIRE(request.read_edge_events(buffer) == 1);
+       auto copy = buffer.get_event(0);
+
+       SECTION("copy constructor works")
+       {
+               auto copy(event);
+               REQUIRE(copy.line_offset() == 0);
+               REQUIRE(copy.type() == event_type::RISING_EDGE);
+               REQUIRE(event.line_offset() == 0);
+               REQUIRE(event.type() == event_type::RISING_EDGE);
+       }
+
+       SECTION("move constructor works")
+       {
+               auto copy(::std::move(event));
+               REQUIRE(copy.line_offset() == 0);
+               REQUIRE(copy.type() == event_type::RISING_EDGE);
+       }
+
+       SECTION("assignment operator works")
+       {
+               copy = event;
+               REQUIRE(copy.line_offset() == 0);
+               REQUIRE(copy.type() == event_type::RISING_EDGE);
+               REQUIRE(event.line_offset() == 0);
+               REQUIRE(event.type() == event_type::RISING_EDGE);
+       }
+
+       SECTION("move assignment operator works")
+       {
+               copy = ::std::move(event);
+               REQUIRE(copy.line_offset() == 0);
+               REQUIRE(copy.type() == event_type::RISING_EDGE);
+       }
+}
+
+TEST_CASE("stream insertion operators work for edge_event and edge_event_buffer", "[edge-event]")
+{
+       /*
+        * This tests the stream insertion operators for both edge_event and
+        * edge_event_buffer classes.
+        */
+
+       auto sim = make_sim().build();
+       ::gpiod::chip chip(sim.dev_path());
+       ::gpiod::edge_event_buffer buffer;
+       ::std::stringstream sbuf, expected;
+
+       auto request = chip
+               .prepare_request()
+               .add_line_settings(
+                       0,
+                       ::gpiod::line_settings()
+                               .set_edge_detection(edge::BOTH)
+               )
+               .do_request();
+
+       sim.set_pull(0, pull::PULL_UP);
+       ::std::this_thread::sleep_for(::std::chrono::milliseconds(30));
+       sim.set_pull(0, pull::PULL_DOWN);
+       ::std::this_thread::sleep_for(::std::chrono::milliseconds(30));
+
+       REQUIRE(request.wait_edge_events(::std::chrono::seconds(1)));
+       REQUIRE(request.read_edge_events(buffer) == 2);
+
+       sbuf << buffer;
+
+       expected << "gpiod::edge_event_buffer\\(num_events=2, capacity=64, events=\\[gpiod::edge_event\\" <<
+                   "(type='RISING_EDGE', timestamp=[1-9][0-9]+, line_offset=0, global_seqno=1, " <<
+                   "line_seqno=1\\), gpiod::edge_event\\(type='FALLING_EDGE', timestamp=[1-9][0-9]+, " <<
+                   "line_offset=0, global_seqno=2, line_seqno=2\\)\\]\\)";
+
+       REQUIRE_THAT(sbuf.str(), regex_matcher(expected.str()));
+}
+
+} /* namespace */
diff --git a/bindings/cxx/tests/tests-info-event.cpp b/bindings/cxx/tests/tests-info-event.cpp
new file mode 100644 (file)
index 0000000..21c0ef0
--- /dev/null
@@ -0,0 +1,225 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+// SPDX-FileCopyrightText: 2022 Bartosz Golaszewski <brgl@bgdev.pl>
+
+#include <catch2/catch.hpp>
+#include <chrono>
+#include <filesystem>
+#include <gpiod.hpp>
+#include <sstream>
+#include <thread>
+#include <utility>
+
+#include "gpiosim.hpp"
+#include "helpers.hpp"
+
+using ::gpiosim::make_sim;
+using direction = ::gpiod::line::direction;
+using event_type = ::gpiod::info_event::event_type;
+
+namespace {
+
+void request_reconfigure_release_line(const ::std::filesystem::path& chip_path)
+{
+       ::std::this_thread::sleep_for(::std::chrono::milliseconds(10));
+
+       auto request = ::gpiod::chip(chip_path)
+               .prepare_request()
+               .add_line_settings(7, ::gpiod::line_settings())
+               .do_request();
+
+       ::std::this_thread::sleep_for(::std::chrono::milliseconds(10));
+
+       request.reconfigure_lines(
+               ::gpiod::line_config()
+                       .add_line_settings(
+                               7,
+                               ::gpiod::line_settings()
+                                       .set_direction(direction::OUTPUT)
+                       )
+       );
+
+       ::std::this_thread::sleep_for(::std::chrono::milliseconds(10));
+
+       request.release();
+}
+
+TEST_CASE("Lines can be watched", "[info-event][chip]")
+{
+       auto sim = make_sim()
+               .set_num_lines(8)
+               .build();
+
+       const auto chip_path = sim.dev_path();
+
+       ::gpiod::chip chip(chip_path);
+
+       SECTION("watch_line_info() returns line info")
+       {
+               auto info = chip.watch_line_info(7);
+               REQUIRE(info.offset() == 7);
+       }
+
+       SECTION("watch_line_info() fails for offset out of range")
+       {
+               REQUIRE_THROWS_AS(chip.watch_line_info(8), ::std::invalid_argument);
+       }
+
+       SECTION("waiting for event timeout")
+       {
+               chip.watch_line_info(3);
+               REQUIRE_FALSE(chip.wait_info_event(::std::chrono::milliseconds(100)));
+       }
+
+       SECTION("request-reconfigure-release events")
+       {
+               auto info = chip.watch_line_info(7);
+               ::std::uint64_t ts_req, ts_rec, ts_rel;
+
+               REQUIRE(info.direction() == direction::INPUT);
+
+               ::std::thread thread(request_reconfigure_release_line, ::std::ref(chip_path));
+
+               REQUIRE(chip.wait_info_event(::std::chrono::seconds(1)));
+               auto event = chip.read_info_event();
+               REQUIRE(event.type() == event_type::LINE_REQUESTED);
+               REQUIRE(event.get_line_info().direction() == direction::INPUT);
+               ts_req = event.timestamp_ns();
+
+               REQUIRE(chip.wait_info_event(::std::chrono::seconds(1)));
+               event = chip.read_info_event();
+               REQUIRE(event.type() == event_type::LINE_CONFIG_CHANGED);
+               REQUIRE(event.get_line_info().direction() == direction::OUTPUT);
+               ts_rec = event.timestamp_ns();
+
+               REQUIRE(chip.wait_info_event(::std::chrono::seconds(1)));
+               event = chip.read_info_event();
+               REQUIRE(event.type() == event_type::LINE_RELEASED);
+               ts_rel = event.timestamp_ns();
+
+               /* No more events. */
+               REQUIRE_FALSE(chip.wait_info_event(::std::chrono::milliseconds(100)));
+               thread.join();
+
+               /* Check timestamps are really monotonic. */
+               REQUIRE(ts_rel > ts_rec);
+               REQUIRE(ts_rec > ts_req);
+       }
+}
+
+TEST_CASE("line info can be unwatched", "[info-event]")
+{
+       auto sim = make_sim()
+               .set_num_lines(8)
+               .build();
+
+       ::gpiod::chip chip(sim.dev_path());
+
+       auto info = chip.watch_line_info(5);
+
+       auto request = chip
+               .prepare_request()
+               .add_line_settings(5, ::gpiod::line_settings())
+               .do_request();
+
+       REQUIRE(chip.wait_info_event(::std::chrono::seconds(1)));
+       auto event = chip.read_info_event();
+       REQUIRE(event.type() == event_type::LINE_REQUESTED);
+
+       chip.unwatch_line_info(5);
+
+       request.release();
+
+       REQUIRE_FALSE(chip.wait_info_event(::std::chrono::milliseconds(100)));
+}
+
+TEST_CASE("info_event can be copied and moved", "[info-event]")
+{
+       auto sim = make_sim().build();
+       ::gpiod::chip chip(sim.dev_path());
+       ::std::stringstream buf, expected;
+
+       chip.watch_line_info(0);
+
+       auto request = chip
+               .prepare_request()
+               .add_line_settings(0, ::gpiod::line_settings())
+               .do_request();
+
+       REQUIRE(chip.wait_info_event(::std::chrono::seconds(1)));
+       auto event = chip.read_info_event();
+
+       request.release();
+
+       REQUIRE(chip.wait_info_event(::std::chrono::seconds(1)));
+       auto copy = chip.read_info_event();
+
+       SECTION("copy constructor works")
+       {
+               auto copy(event);
+
+               REQUIRE(copy.type() == event_type::LINE_REQUESTED);
+               REQUIRE(copy.get_line_info().offset() == 0);
+
+               REQUIRE(event.type() == event_type::LINE_REQUESTED);
+               REQUIRE(event.get_line_info().offset() == 0);
+       }
+
+       SECTION("assignment operator works")
+       {
+               copy = event;
+
+               REQUIRE(copy.type() == event_type::LINE_REQUESTED);
+               REQUIRE(copy.get_line_info().offset() == 0);
+
+               REQUIRE(event.type() == event_type::LINE_REQUESTED);
+               REQUIRE(event.get_line_info().offset() == 0);
+       }
+
+       SECTION("move constructor works")
+       {
+               auto copy(::std::move(event));
+
+               REQUIRE(copy.type() == event_type::LINE_REQUESTED);
+               REQUIRE(copy.get_line_info().offset() == 0);
+       }
+
+       SECTION("move assignment operator works")
+       {
+               copy = ::std::move(event);
+
+               REQUIRE(copy.type() == event_type::LINE_REQUESTED);
+               REQUIRE(copy.get_line_info().offset() == 0);
+       }
+}
+
+TEST_CASE("info_event stream insertion operator works", "[info-event][line-info]")
+{
+       /*
+        * This tests the stream insertion operator for both the info_event
+        * and line_info classes.
+        */
+
+       auto sim = make_sim().build();
+       ::gpiod::chip chip(sim.dev_path());
+       ::std::stringstream buf, expected;
+
+       chip.watch_line_info(0);
+
+       auto request = chip
+               .prepare_request()
+               .add_line_settings(0, ::gpiod::line_settings())
+               .do_request();
+
+       auto event = chip.read_info_event();
+
+       buf << event;
+
+       expected << "gpiod::info_event\\(event_type='LINE_REQUESTED', timestamp=[1-9][0-9]+, " <<
+                   "line_info=gpiod::line_info\\(offset=0, name=unnamed, used=true, consumer='', " <<
+                   "direction=INPUT, active_low=false, bias=UNKNOWN, drive=PUSH_PULL, " <<
+                   "edge_detection=NONE, event_clock=MONOTONIC, debounced=false\\)\\)";
+
+       REQUIRE_THAT(buf.str(), regex_matcher(expected.str()));
+}
+
+} /* namespace */
diff --git a/bindings/cxx/tests/tests-line-config.cpp b/bindings/cxx/tests/tests-line-config.cpp
new file mode 100644 (file)
index 0000000..5e439a1
--- /dev/null
@@ -0,0 +1,155 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+// SPDX-FileCopyrightText: 2022 Bartosz Golaszewski <brgl@bgdev.pl>
+
+#include <catch2/catch.hpp>
+#include <gpiod.hpp>
+
+#include "gpiosim.hpp"
+#include "helpers.hpp"
+
+using ::gpiosim::make_sim;
+using namespace ::std::chrono_literals;
+using direction = ::gpiod::line::direction;
+using drive = ::gpiod::line::drive;
+using edge = ::gpiod::line::edge;
+using simval = ::gpiosim::chip::value;
+using value = ::gpiod::line::value;
+using values = ::gpiod::line::values;
+
+namespace {
+
+TEST_CASE("line_config constructor works", "[line-config]")
+{
+       SECTION("no arguments - default values")
+       {
+               ::gpiod::line_config cfg;
+
+               REQUIRE(cfg.get_line_settings().size() == 0);
+       }
+}
+
+TEST_CASE("adding line_settings to line_config works", "[line-config][line-settings]")
+{
+       ::gpiod::line_config cfg;
+
+       cfg.add_line_settings(4,
+               ::gpiod::line_settings()
+                       .set_direction(direction::INPUT)
+                       .set_edge_detection(edge::RISING));
+
+       cfg.add_line_settings({7, 2},
+               ::gpiod::line_settings()
+                       .set_direction(direction::OUTPUT)
+                       .set_drive(drive::OPEN_DRAIN));
+
+       auto settings = cfg.get_line_settings();
+
+       REQUIRE(settings.size() == 3);
+       REQUIRE(settings.at(2).direction() == direction::OUTPUT);
+       REQUIRE(settings.at(2).drive() == drive::OPEN_DRAIN);
+       REQUIRE(settings.at(4).direction() == direction::INPUT);
+       REQUIRE(settings.at(4).edge_detection() == edge::RISING);
+       REQUIRE(settings.at(7).direction() == direction::OUTPUT);
+       REQUIRE(settings.at(7).drive() == drive::OPEN_DRAIN);
+}
+
+TEST_CASE("line_config can be reset", "[line-config]")
+{
+       ::gpiod::line_config cfg;
+
+       cfg.add_line_settings({3, 4, 7},
+               ::gpiod::line_settings()
+                       .set_direction(direction::INPUT)
+                       .set_edge_detection(edge::BOTH));
+
+       auto settings = cfg.get_line_settings();
+
+       REQUIRE(settings.size() == 3);
+       REQUIRE(settings.at(3).direction() == direction::INPUT);
+       REQUIRE(settings.at(3).edge_detection() == edge::BOTH);
+       REQUIRE(settings.at(4).direction() == direction::INPUT);
+       REQUIRE(settings.at(4).edge_detection() == edge::BOTH);
+       REQUIRE(settings.at(7).direction() == direction::INPUT);
+       REQUIRE(settings.at(7).edge_detection() == edge::BOTH);
+
+       cfg.reset();
+
+       REQUIRE(cfg.get_line_settings().size() == 0);
+}
+
+TEST_CASE("output values can be set globally", "[line-config]")
+{
+       const values vals = { value::ACTIVE, value::INACTIVE, value::ACTIVE, value::INACTIVE };
+
+       auto sim = make_sim()
+               .set_num_lines(4)
+               .build();
+
+       ::gpiod::line_config cfg;
+
+       SECTION("request with globally set output values")
+       {
+               cfg
+                       .add_line_settings(
+                               {0, 1, 2, 3},
+                               ::gpiod::line_settings().set_direction(direction::OUTPUT)
+                       )
+                       .set_output_values(vals);
+
+               auto request = ::gpiod::chip(sim.dev_path())
+                       .prepare_request()
+                       .set_line_config(cfg)
+                       .do_request();
+
+               REQUIRE(sim.get_value(0) == simval::ACTIVE);
+               REQUIRE(sim.get_value(1) == simval::INACTIVE);
+               REQUIRE(sim.get_value(2) == simval::ACTIVE);
+               REQUIRE(sim.get_value(3) == simval::INACTIVE);
+       }
+
+       SECTION("read back global output values")
+       {
+               cfg
+                       .add_line_settings(
+                               {0, 1, 2, 3},
+                               ::gpiod::line_settings()
+                                       .set_direction(direction::OUTPUT)
+                                       .set_output_value(value::ACTIVE)
+                       )
+                       .set_output_values(vals);
+
+               auto settings = cfg.get_line_settings()[1];
+               REQUIRE(settings.output_value() == value::INACTIVE);
+       }
+}
+
+TEST_CASE("line_config stream insertion operator works", "[line-config]")
+{
+       ::gpiod::line_config cfg;
+
+       SECTION("empty config")
+       {
+               REQUIRE_THAT(cfg, stringify_matcher<::gpiod::line_config>(
+                                       "gpiod::line_config(num_settings=0)"));
+       }
+
+       SECTION("config with settings")
+       {
+               cfg.add_line_settings({0, 2},
+                               ::gpiod::line_settings()
+                                       .set_direction(direction::OUTPUT)
+                                       .set_drive(drive::OPEN_SOURCE)
+               );
+
+               REQUIRE_THAT(cfg, stringify_matcher<::gpiod::line_config>(
+                       "gpiod::line_config(num_settings=2, "
+                       "settings=[0: gpiod::line_settings(direction=OUTPUT, edge_detection=NONE, "
+                       "bias=AS_IS, drive=OPEN_SOURCE, active-high, debounce_period=0, "
+                       "event_clock=MONOTONIC, output_value=INACTIVE), "
+                       "2: gpiod::line_settings(direction=OUTPUT, edge_detection=NONE, bias=AS_IS, "
+                       "drive=OPEN_SOURCE, active-high, debounce_period=0, event_clock=MONOTONIC, "
+                       "output_value=INACTIVE)])"));
+       }
+}
+
+} /* namespace */
diff --git a/bindings/cxx/tests/tests-line-info.cpp b/bindings/cxx/tests/tests-line-info.cpp
new file mode 100644 (file)
index 0000000..21211f2
--- /dev/null
@@ -0,0 +1,154 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+// SPDX-FileCopyrightText: 2022 Bartosz Golaszewski <brgl@bgdev.pl>
+
+#include <catch2/catch.hpp>
+#include <gpiod.hpp>
+#include <string>
+
+#include "helpers.hpp"
+#include "gpiosim.hpp"
+
+using ::gpiosim::make_sim;
+using hog_dir = ::gpiosim::chip_builder::direction;
+using direction = ::gpiod::line::direction;
+using edge = ::gpiod::line::edge;
+using bias = ::gpiod::line::bias;
+using drive = ::gpiod::line::drive;
+using event_clock = ::gpiod::line::clock;
+
+using namespace ::std::chrono_literals;
+
+namespace {
+
+TEST_CASE("get_line_info() works", "[chip][line-info]")
+{
+       auto sim = make_sim()
+               .set_num_lines(8)
+               .set_line_name(0,  "foobar")
+               .set_hog(0, "hog", hog_dir::OUTPUT_HIGH)
+               .build();
+
+       ::gpiod::chip chip(sim.dev_path());
+
+       SECTION("line_info can be retrieved from chip")
+       {
+               auto info = chip.get_line_info(0);
+
+               REQUIRE(info.offset() == 0);
+               REQUIRE_THAT(info.name(), Catch::Equals("foobar"));
+               REQUIRE(info.used());
+               REQUIRE_THAT(info.consumer(), Catch::Equals("hog"));
+               REQUIRE(info.direction() == ::gpiod::line::direction::OUTPUT);
+               REQUIRE_FALSE(info.active_low());
+               REQUIRE(info.bias() == ::gpiod::line::bias::UNKNOWN);
+               REQUIRE(info.drive() == ::gpiod::line::drive::PUSH_PULL);
+               REQUIRE(info.edge_detection() == ::gpiod::line::edge::NONE);
+               REQUIRE(info.event_clock() == ::gpiod::line::clock::MONOTONIC);
+               REQUIRE_FALSE(info.debounced());
+               REQUIRE(info.debounce_period() == 0us);
+       }
+
+       SECTION("offset out of range")
+       {
+               REQUIRE_THROWS_AS(chip.get_line_info(8), ::std::invalid_argument);
+       }
+}
+
+TEST_CASE("line properties can be retrieved", "[line-info]")
+{
+       auto sim = make_sim()
+               .set_num_lines(8)
+               .set_line_name(1, "foo")
+               .set_line_name(2, "bar")
+               .set_line_name(4, "baz")
+               .set_line_name(5, "xyz")
+               .set_hog(3, "hog3", hog_dir::OUTPUT_HIGH)
+               .set_hog(4, "hog4", hog_dir::OUTPUT_LOW)
+               .build();
+
+       ::gpiod::chip chip(sim.dev_path());
+
+       SECTION("basic properties")
+       {
+               auto info4 = chip.get_line_info(4);
+               auto info6 = chip.get_line_info(6);
+
+               REQUIRE(info4.offset() == 4);
+               REQUIRE_THAT(info4.name(), Catch::Equals("baz"));
+               REQUIRE(info4.used());
+               REQUIRE_THAT(info4.consumer(), Catch::Equals("hog4"));
+               REQUIRE(info4.direction() == direction::OUTPUT);
+               REQUIRE(info4.edge_detection() == edge::NONE);
+               REQUIRE_FALSE(info4.active_low());
+               REQUIRE(info4.bias() == bias::UNKNOWN);
+               REQUIRE(info4.drive() == drive::PUSH_PULL);
+               REQUIRE(info4.event_clock() == event_clock::MONOTONIC);
+               REQUIRE_FALSE(info4.debounced());
+               REQUIRE(info4.debounce_period() == 0us);
+       }
+}
+
+TEST_CASE("line_info can be copied and moved")
+{
+       auto sim = make_sim()
+               .set_num_lines(4)
+               .set_line_name(2, "foobar")
+               .build();
+
+       ::gpiod::chip chip(sim.dev_path());
+       auto info = chip.get_line_info(2);
+
+       SECTION("copy constructor works")
+       {
+               auto copy(info);
+               REQUIRE(copy.offset() == 2);
+               REQUIRE_THAT(copy.name(), Catch::Equals("foobar"));
+               /* info can still be used */
+               REQUIRE(info.offset() == 2);
+               REQUIRE_THAT(info.name(), Catch::Equals("foobar"));
+       }
+
+       SECTION("assignment operator works")
+       {
+               auto copy = chip.get_line_info(0);
+               copy = info;
+               REQUIRE(copy.offset() == 2);
+               REQUIRE_THAT(copy.name(), Catch::Equals("foobar"));
+               /* info can still be used */
+               REQUIRE(info.offset() == 2);
+               REQUIRE_THAT(info.name(), Catch::Equals("foobar"));
+       }
+
+       SECTION("move constructor works")
+       {
+               auto copy(::std::move(info));
+               REQUIRE(copy.offset() == 2);
+               REQUIRE_THAT(copy.name(), Catch::Equals("foobar"));
+       }
+
+       SECTION("move assignment operator works")
+       {
+               auto copy = chip.get_line_info(0);
+               copy = ::std::move(info);
+               REQUIRE(copy.offset() == 2);
+               REQUIRE_THAT(copy.name(), Catch::Equals("foobar"));
+       }
+}
+
+TEST_CASE("line_info stream insertion operator works")
+{
+       auto sim = make_sim()
+               .set_line_name(0, "foo")
+               .set_hog(0, "hogger", hog_dir::OUTPUT_HIGH)
+               .build();
+
+       ::gpiod::chip chip(sim.dev_path());
+
+       auto info = chip.get_line_info(0);
+
+       REQUIRE_THAT(info, stringify_matcher<::gpiod::line_info>(
+               "gpiod::line_info(offset=0, name='foo', used=true, consumer='foo', direction=OUTPUT, "
+               "active_low=false, bias=UNKNOWN, drive=PUSH_PULL, edge_detection=NONE, event_clock=MONOTONIC, debounced=false)"));
+}
+
+} /* namespace */
diff --git a/bindings/cxx/tests/tests-line-request.cpp b/bindings/cxx/tests/tests-line-request.cpp
new file mode 100644 (file)
index 0000000..6e29532
--- /dev/null
@@ -0,0 +1,501 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+// SPDX-FileCopyrightText: 2022 Bartosz Golaszewski <brgl@bgdev.pl>
+
+#include <catch2/catch.hpp>
+#include <gpiod.hpp>
+#include <sstream>
+#include <stdexcept>
+#include <vector>
+
+#include "gpiosim.hpp"
+#include "helpers.hpp"
+
+using ::gpiosim::make_sim;
+using offsets = ::gpiod::line::offsets;
+using values = ::gpiod::line::values;
+using direction = ::gpiod::line::direction;
+using value = ::gpiod::line::value;
+using simval = ::gpiosim::chip::value;
+using pull = ::gpiosim::chip::pull;
+
+namespace {
+
+class value_matcher : public Catch::MatcherBase<value>
+{
+public:
+       value_matcher(pull pull, bool active_low = false)
+               : _m_pull(pull),
+                 _m_active_low(active_low)
+       {
+
+       }
+
+       ::std::string describe() const override
+       {
+               ::std::string repr(this->_m_pull == pull::PULL_UP ? "PULL_UP" : "PULL_DOWN");
+               ::std::string active_low = this->_m_active_low ? "(active-low) " : "";
+
+               return active_low + "corresponds with " + repr;
+       }
+
+       bool match(const value& val) const override
+       {
+               if (this->_m_active_low) {
+                       if ((val == value::ACTIVE && this->_m_pull == pull::PULL_DOWN) ||
+                           (val == value::INACTIVE && this->_m_pull == pull::PULL_UP))
+                               return true;
+               } else {
+                       if ((val == value::ACTIVE && this->_m_pull == pull::PULL_UP) ||
+                           (val == value::INACTIVE && this->_m_pull == pull::PULL_DOWN))
+                               return true;
+               }
+
+               return false;
+       }
+
+private:
+       pull _m_pull;
+       bool _m_active_low;
+};
+
+TEST_CASE("requesting lines behaves correctly with invalid arguments", "[line-request][chip]")
+{
+       auto sim = make_sim()
+               .set_num_lines(8)
+               .build();
+
+       ::gpiod::chip chip(sim.dev_path());
+
+       SECTION("no offsets")
+       {
+               REQUIRE_THROWS_AS(chip.prepare_request().do_request(), ::std::invalid_argument);
+       }
+
+       SECTION("duplicate offsets")
+       {
+               auto request = chip
+                       .prepare_request()
+                       .add_line_settings({ 2, 0, 0, 4 }, ::gpiod::line_settings())
+                       .do_request();
+
+               auto offsets = request.offsets();
+
+               REQUIRE(offsets.size() == 3);
+               REQUIRE(offsets[0] == 2);
+               REQUIRE(offsets[1] == 0);
+               REQUIRE(offsets[2] == 4);
+       }
+
+       SECTION("offset out of bounds")
+       {
+               REQUIRE_THROWS_AS(chip
+                       .prepare_request()
+                       .add_line_settings({ 2, 0, 8, 4 }, ::gpiod::line_settings())
+                       .do_request(),
+                       ::std::invalid_argument
+               );
+       }
+}
+
+TEST_CASE("consumer string is set correctly", "[line-request]")
+{
+       auto sim = make_sim()
+               .set_num_lines(4)
+               .build();
+
+       ::gpiod::chip chip(sim.dev_path());
+       offsets offs({ 3, 0, 2 });
+
+       SECTION("set custom consumer")
+       {
+               auto request = chip
+                       .prepare_request()
+                       .add_line_settings(offs, ::gpiod::line_settings())
+                       .set_consumer("foobar")
+                       .do_request();
+
+               auto info = chip.get_line_info(2);
+
+               REQUIRE(info.used());
+               REQUIRE_THAT(info.consumer(), Catch::Equals("foobar"));
+       }
+
+       SECTION("empty consumer")
+       {
+               auto request = chip
+                       .prepare_request()
+                       .add_line_settings(2, ::gpiod::line_settings())
+                       .do_request();
+
+               auto info = chip.get_line_info(2);
+
+               REQUIRE(info.used());
+               REQUIRE_THAT(info.consumer(), Catch::Equals("?"));
+       }
+}
+
+TEST_CASE("values can be read", "[line-request]")
+{
+       auto sim = make_sim()
+               .set_num_lines(8)
+               .build();
+
+       const offsets offs({ 7, 1, 0, 6, 2 });
+
+       const ::std::vector<pull> pulls({
+               pull::PULL_UP,
+               pull::PULL_UP,
+               pull::PULL_DOWN,
+               pull::PULL_UP,
+               pull::PULL_DOWN
+       });
+
+       for (unsigned int i = 0; i < offs.size(); i++)
+               sim.set_pull(offs[i], pulls[i]);
+
+       auto request = ::gpiod::chip(sim.dev_path())
+               .prepare_request()
+               .add_line_settings(
+                       offs,
+                       ::gpiod::line_settings()
+                               .set_direction(direction::INPUT)
+               )
+               .do_request();
+
+       SECTION("get all values (returning variant)")
+       {
+               auto vals = request.get_values();
+
+               REQUIRE_THAT(vals[0], value_matcher(pull::PULL_UP));
+               REQUIRE_THAT(vals[1], value_matcher(pull::PULL_UP));
+               REQUIRE_THAT(vals[2], value_matcher(pull::PULL_DOWN));
+               REQUIRE_THAT(vals[3], value_matcher(pull::PULL_UP));
+               REQUIRE_THAT(vals[4], value_matcher(pull::PULL_DOWN));
+       }
+
+       SECTION("get all values (passed buffer variant)")
+       {
+               values vals(5);
+
+               request.get_values(vals);
+
+               REQUIRE_THAT(vals[0], value_matcher(pull::PULL_UP));
+               REQUIRE_THAT(vals[1], value_matcher(pull::PULL_UP));
+               REQUIRE_THAT(vals[2], value_matcher(pull::PULL_DOWN));
+               REQUIRE_THAT(vals[3], value_matcher(pull::PULL_UP));
+               REQUIRE_THAT(vals[4], value_matcher(pull::PULL_DOWN));
+       }
+
+       SECTION("get_values(buffer) throws for invalid buffer size")
+       {
+               values vals(4);
+               REQUIRE_THROWS_AS(request.get_values(vals), ::std::invalid_argument);
+               vals.resize(6);
+               REQUIRE_THROWS_AS(request.get_values(vals), ::std::invalid_argument);
+       }
+
+       SECTION("get a single value")
+       {
+               auto val = request.get_value(7);
+
+               REQUIRE_THAT(val, value_matcher(pull::PULL_UP));
+       }
+
+       SECTION("get a single value (active-low)")
+       {
+               request.reconfigure_lines(
+                       ::gpiod::line_config()
+                               .add_line_settings(
+                                       offs,
+                                       ::gpiod::line_settings()
+                                               .set_direction(direction::INPUT)
+                                               .set_active_low(true))
+               );
+
+               auto val = request.get_value(7);
+
+               REQUIRE_THAT(val, value_matcher(pull::PULL_UP, true));
+       }
+
+       SECTION("get a subset of values (returning variant)")
+       {
+               auto vals = request.get_values(offsets({ 2, 0, 6 }));
+
+               REQUIRE_THAT(vals[0], value_matcher(pull::PULL_DOWN));
+               REQUIRE_THAT(vals[1], value_matcher(pull::PULL_DOWN));
+               REQUIRE_THAT(vals[2], value_matcher(pull::PULL_UP));
+       }
+
+       SECTION("get a subset of values (passed buffer variant)")
+       {
+               values vals(3);
+
+               request.get_values(offsets({ 2, 0, 6 }), vals);
+
+               REQUIRE_THAT(vals[0], value_matcher(pull::PULL_DOWN));
+               REQUIRE_THAT(vals[1], value_matcher(pull::PULL_DOWN));
+               REQUIRE_THAT(vals[2], value_matcher(pull::PULL_UP));
+       }
+}
+
+TEST_CASE("output values can be set at request time", "[line-request]")
+{
+       auto sim = make_sim()
+               .set_num_lines(8)
+               .build();
+
+       ::gpiod::chip chip(sim.dev_path());
+       const offsets offs({ 0, 1, 3, 4 });
+
+       ::gpiod::line_settings settings;
+       settings.set_direction(direction::OUTPUT);
+       settings.set_output_value(value::ACTIVE);
+
+       ::gpiod::line_config line_cfg;
+       line_cfg.add_line_settings(offs, settings);
+
+       SECTION("default output value")
+       {
+               auto request = chip
+                       .prepare_request()
+                       .set_line_config(line_cfg)
+                       .do_request();
+
+               for (const auto& off: offs)
+                       REQUIRE(sim.get_value(off) == simval::ACTIVE);
+
+               REQUIRE(sim.get_value(2) == simval::INACTIVE);
+       }
+
+       SECTION("overridden output value")
+       {
+               settings.set_output_value(value::INACTIVE);
+               line_cfg.add_line_settings(1, settings);
+
+               auto request = chip
+                       .prepare_request()
+                       .set_line_config(line_cfg)
+                       .do_request();
+
+               REQUIRE(sim.get_value(0) == simval::ACTIVE);
+               REQUIRE(sim.get_value(1) == simval::INACTIVE);
+               REQUIRE(sim.get_value(2) == simval::INACTIVE);
+               REQUIRE(sim.get_value(3) == simval::ACTIVE);
+               REQUIRE(sim.get_value(4) == simval::ACTIVE);
+       }
+}
+
+TEST_CASE("values can be set after requesting lines", "[line-request]")
+{
+       auto sim = make_sim()
+               .set_num_lines(8)
+               .build();
+
+       const offsets offs({ 0, 1, 3, 4 });
+
+       auto request = ::gpiod::chip(sim.dev_path())
+               .prepare_request()
+               .add_line_settings(
+                       offs,
+                       ::gpiod::line_settings()
+                               .set_direction(direction::OUTPUT)
+               )
+               .do_request();
+
+       SECTION("set single value")
+       {
+               request.set_value(1, value::ACTIVE);
+
+               REQUIRE(sim.get_value(0) == simval::INACTIVE);
+               REQUIRE(sim.get_value(1) == simval::ACTIVE);
+               REQUIRE(sim.get_value(3) == simval::INACTIVE);
+               REQUIRE(sim.get_value(4) == simval::INACTIVE);
+       }
+
+       SECTION("set all values")
+       {
+               request.set_values({
+                       value::ACTIVE,
+                       value::INACTIVE,
+                       value::ACTIVE,
+                       value::INACTIVE
+               });
+
+               REQUIRE(sim.get_value(0) == simval::ACTIVE);
+               REQUIRE(sim.get_value(1) == simval::INACTIVE);
+               REQUIRE(sim.get_value(3) == simval::ACTIVE);
+               REQUIRE(sim.get_value(4) == simval::INACTIVE);
+       }
+
+       SECTION("set a subset of values")
+       {
+               request.set_values({ 4, 3 }, { value::ACTIVE, value::INACTIVE });
+
+               REQUIRE(sim.get_value(0) == simval::INACTIVE);
+               REQUIRE(sim.get_value(1) == simval::INACTIVE);
+               REQUIRE(sim.get_value(3) == simval::INACTIVE);
+               REQUIRE(sim.get_value(4) == simval::ACTIVE);
+       }
+
+       SECTION("set a subset of values with mappings")
+       {
+               request.set_values({
+                       { 0, value::ACTIVE },
+                       { 4, value::INACTIVE },
+                       { 1, value::ACTIVE}
+               });
+
+               REQUIRE(sim.get_value(0) == simval::ACTIVE);
+               REQUIRE(sim.get_value(1) == simval::ACTIVE);
+               REQUIRE(sim.get_value(3) == simval::INACTIVE);
+               REQUIRE(sim.get_value(4) == simval::INACTIVE);
+       }
+}
+
+TEST_CASE("line_request can be moved", "[line-request]")
+{
+       auto sim = make_sim()
+               .set_num_lines(8)
+               .build();
+
+       ::gpiod::chip chip(sim.dev_path());
+       const offsets offs({ 3, 1, 0, 2 });
+
+       auto request = chip
+               .prepare_request()
+               .add_line_settings(
+                       offs,
+                       ::gpiod::line_settings()
+               )
+               .do_request();
+
+       auto fd = request.fd();
+
+       auto another = chip
+               .prepare_request()
+               .add_line_settings(6, ::gpiod::line_settings())
+               .do_request();
+
+       SECTION("move constructor works")
+       {
+               auto moved(::std::move(request));
+
+               REQUIRE(moved.fd() == fd);
+               REQUIRE_THAT(moved.offsets(), Catch::Equals(offs));
+       }
+
+       SECTION("move assignment operator works")
+       {
+               another = ::std::move(request);
+
+               REQUIRE(another.fd() == fd);
+               REQUIRE_THAT(another.offsets(), Catch::Equals(offs));
+       }
+}
+
+TEST_CASE("released request can no longer be used", "[line-request]")
+{
+       auto sim = make_sim().build();
+
+       auto request = ::gpiod::chip(sim.dev_path())
+               .prepare_request()
+               .add_line_settings(0, ::gpiod::line_settings())
+               .do_request();
+
+       request.release();
+
+       REQUIRE_THROWS_AS(request.offsets(), ::gpiod::request_released);
+}
+
+TEST_CASE("line_request survives parent chip", "[line-request][chip]")
+{
+       auto sim = make_sim().build();
+
+       sim.set_pull(0, pull::PULL_UP);
+
+       SECTION("chip is released")
+       {
+               ::gpiod::chip chip(sim.dev_path());
+
+               auto request = chip
+                       .prepare_request()
+                       .add_line_settings(
+                               0,
+                               ::gpiod::line_settings()
+                                       .set_direction(direction::INPUT)
+                       )
+                       .do_request();
+
+               REQUIRE_THAT(request.get_value(0), value_matcher(pull::PULL_UP));
+
+               chip.close();
+
+               REQUIRE_THAT(request.get_value(0), value_matcher(pull::PULL_UP));
+       }
+
+       SECTION("chip goes out of scope")
+       {
+               /* Need to get the request object somehow. */
+               ::gpiod::chip dummy(sim.dev_path());
+               ::gpiod::line_config cfg;
+               cfg.add_line_settings(0, ::gpiod::line_settings().set_direction(direction::INPUT));
+
+               auto request = dummy
+                       .prepare_request()
+                       .set_line_config(cfg)
+                       .do_request();
+
+               request.release();
+               dummy.close();
+
+               {
+                       ::gpiod::chip chip(sim.dev_path());
+
+                       request = chip
+                               .prepare_request()
+                               .set_line_config(cfg)
+                               .do_request();
+
+                       REQUIRE_THAT(request.get_value(0), value_matcher(pull::PULL_UP));
+               }
+
+               REQUIRE_THAT(request.get_value(0), value_matcher(pull::PULL_UP));
+       }
+}
+
+TEST_CASE("line_request stream insertion operator works", "[line-request]")
+{
+       auto sim = make_sim()
+               .set_num_lines(4)
+               .build();
+
+       auto chip = ::gpiod::chip(sim.dev_path());
+       auto request = chip
+               .prepare_request()
+               .add_line_settings({ 3, 1, 0, 2}, ::gpiod::line_settings())
+               .do_request();
+
+       ::std::stringstream buf, expected;
+
+       expected << "gpiod::line_request(chip=\"" << sim.name() <<
+                   "\", num_lines=4, line_offsets=gpiod::offsets(3, 1, 0, 2), fd=" <<
+                   request.fd() << ")";
+
+       SECTION("active request")
+       {
+               buf << request;
+
+               REQUIRE_THAT(buf.str(), Catch::Equals(expected.str()));
+       }
+
+       SECTION("request released")
+       {
+               request.release();
+
+               buf << request;
+
+               REQUIRE_THAT(buf.str(), Catch::Equals("gpiod::line_request(released)"));
+       }
+}
+
+} /* namespace */
diff --git a/bindings/cxx/tests/tests-line-settings.cpp b/bindings/cxx/tests/tests-line-settings.cpp
new file mode 100644 (file)
index 0000000..dc821bb
--- /dev/null
@@ -0,0 +1,188 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+// SPDX-FileCopyrightText: 2022 Bartosz Golaszewski <brgl@bgdev.pl>
+
+#include <catch2/catch.hpp>
+#include <gpiod.hpp>
+
+#include "helpers.hpp"
+
+using value = ::gpiod::line::value;
+using direction = ::gpiod::line::direction;
+using edge = ::gpiod::line::edge;
+using bias = ::gpiod::line::bias;
+using drive = ::gpiod::line::drive;
+using clock_type = ::gpiod::line::clock;
+using value = ::gpiod::line::value;
+
+using namespace ::std::chrono_literals;
+
+namespace {
+
+TEST_CASE("line_settings constructor works", "[line-settings]")
+{
+       ::gpiod::line_settings settings;
+
+       REQUIRE(settings.direction() == direction::AS_IS);
+       REQUIRE(settings.edge_detection() == edge::NONE);
+       REQUIRE(settings.bias() == bias::AS_IS);
+       REQUIRE(settings.drive() == drive::PUSH_PULL);
+       REQUIRE_FALSE(settings.active_low());
+       REQUIRE(settings.debounce_period() == 0us);
+       REQUIRE(settings.event_clock() == clock_type::MONOTONIC);
+       REQUIRE(settings.output_value() == value::INACTIVE);
+}
+
+TEST_CASE("line_settings mutators work", "[line-settings]")
+{
+       ::gpiod::line_settings settings;
+
+       SECTION("direction")
+       {
+               settings.set_direction(direction::INPUT);
+               REQUIRE(settings.direction() == direction::INPUT);
+               settings.set_direction(direction::AS_IS);
+               REQUIRE(settings.direction() == direction::AS_IS);
+               settings.set_direction(direction::OUTPUT);
+               REQUIRE(settings.direction() == direction::OUTPUT);
+               REQUIRE_THROWS_AS(settings.set_direction(static_cast<direction>(999)),
+                                 ::std::invalid_argument);
+       }
+
+       SECTION("edge detection")
+       {
+               settings.set_edge_detection(edge::BOTH);
+               REQUIRE(settings.edge_detection() == edge::BOTH);
+               settings.set_edge_detection(edge::NONE);
+               REQUIRE(settings.edge_detection() == edge::NONE);
+               settings.set_edge_detection(edge::FALLING);
+               REQUIRE(settings.edge_detection() == edge::FALLING);
+               settings.set_edge_detection(edge::RISING);
+               REQUIRE(settings.edge_detection() == edge::RISING);
+               REQUIRE_THROWS_AS(settings.set_edge_detection(static_cast<edge>(999)),
+                                 ::std::invalid_argument);
+       }
+
+       SECTION("bias")
+       {
+               settings.set_bias(bias::DISABLED);
+               REQUIRE(settings.bias() == bias::DISABLED);
+               settings.set_bias(bias::AS_IS);
+               REQUIRE(settings.bias() == bias::AS_IS);
+               settings.set_bias(bias::PULL_DOWN);
+               REQUIRE(settings.bias() == bias::PULL_DOWN);
+               settings.set_bias(bias::PULL_UP);
+               REQUIRE(settings.bias() == bias::PULL_UP);
+               REQUIRE_THROWS_AS(settings.set_bias(static_cast<bias>(999)), ::std::invalid_argument);
+               REQUIRE_THROWS_AS(settings.set_bias(bias::UNKNOWN), ::std::invalid_argument);
+       }
+
+       SECTION("drive")
+       {
+               settings.set_drive(drive::OPEN_DRAIN);
+               REQUIRE(settings.drive() == drive::OPEN_DRAIN);
+               settings.set_drive(drive::PUSH_PULL);
+               REQUIRE(settings.drive() == drive::PUSH_PULL);
+               settings.set_drive(drive::OPEN_SOURCE);
+               REQUIRE(settings.drive() == drive::OPEN_SOURCE);
+               REQUIRE_THROWS_AS(settings.set_drive(static_cast<drive>(999)), ::std::invalid_argument);
+       }
+
+       SECTION("active-low")
+       {
+               settings.set_active_low(true);
+               REQUIRE(settings.active_low());
+               settings.set_active_low(false);
+               REQUIRE_FALSE(settings.active_low());
+       }
+
+       SECTION("debounce period")
+       {
+               settings.set_debounce_period(2000us);
+               REQUIRE(settings.debounce_period() == 2000us);
+       }
+
+       SECTION("event clock")
+       {
+               settings.set_event_clock(clock_type::REALTIME);
+               REQUIRE(settings.event_clock() == clock_type::REALTIME);
+               settings.set_event_clock(clock_type::MONOTONIC);
+               REQUIRE(settings.event_clock() == clock_type::MONOTONIC);
+               settings.set_event_clock(clock_type::HTE);
+               REQUIRE(settings.event_clock() == clock_type::HTE);
+               REQUIRE_THROWS_AS(settings.set_event_clock(static_cast<clock_type>(999)),
+                                 ::std::invalid_argument);
+       }
+
+       SECTION("output value")
+       {
+               settings.set_output_value(value::ACTIVE);
+               REQUIRE(settings.output_value() == value::ACTIVE);
+               settings.set_output_value(value::INACTIVE);
+               REQUIRE(settings.output_value() == value::INACTIVE);
+               REQUIRE_THROWS_AS(settings.set_output_value(static_cast<value>(999)),
+                                 ::std::invalid_argument);
+       }
+}
+
+TEST_CASE("line_settings can be moved and copied", "[line-settings]")
+{
+       ::gpiod::line_settings settings;
+
+       settings
+               .set_direction(direction::INPUT)
+               .set_edge_detection(edge::BOTH);
+
+       SECTION("copy constructor works")
+       {
+               auto copy(settings);
+               settings.set_direction(direction::OUTPUT);
+               settings.set_edge_detection(edge::NONE);
+               REQUIRE(copy.direction() == direction::INPUT);
+               REQUIRE(copy.edge_detection() == edge::BOTH);
+       }
+
+       SECTION("assignment operator works")
+       {
+               ::gpiod::line_settings copy;
+               copy = settings;
+               settings.set_direction(direction::OUTPUT);
+               settings.set_edge_detection(edge::NONE);
+               REQUIRE(copy.direction() == direction::INPUT);
+               REQUIRE(copy.edge_detection() == edge::BOTH);
+       }
+
+       SECTION("move constructor works")
+       {
+               auto copy(::std::move(settings));
+               REQUIRE(copy.direction() == direction::INPUT);
+               REQUIRE(copy.edge_detection() == edge::BOTH);
+       }
+
+       SECTION("move assignment operator works")
+       {
+               ::gpiod::line_settings copy;
+               copy = ::std::move(settings);
+               REQUIRE(copy.direction() == direction::INPUT);
+               REQUIRE(copy.edge_detection() == edge::BOTH);
+       }
+}
+
+TEST_CASE("line_settings stream insertion operator works", "[line-settings]")
+{
+       ::gpiod::line_settings settings;
+
+       REQUIRE_THAT(settings
+               .set_active_low(true)
+               .set_direction(direction::INPUT)
+               .set_edge_detection(edge::BOTH)
+               .set_bias(bias::PULL_DOWN)
+               .set_event_clock(clock_type::REALTIME),
+               stringify_matcher<::gpiod::line_settings>(
+                       "gpiod::line_settings(direction=INPUT, edge_detection=BOTH_EDGES, "
+                       "bias=PULL_DOWN, drive=PUSH_PULL, active-low, debounce_period=0, "
+                       "event_clock=REALTIME, output_value=INACTIVE)"
+               )
+       );
+}
+
+} /* namespace */
diff --git a/bindings/cxx/tests/tests-line.cpp b/bindings/cxx/tests/tests-line.cpp
new file mode 100644 (file)
index 0000000..319012a
--- /dev/null
@@ -0,0 +1,139 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+// SPDX-FileCopyrightText: 2021-2022 Bartosz Golaszewski <brgl@bgdev.pl>
+
+#include <catch2/catch.hpp>
+#include <gpiod.hpp>
+
+#include "helpers.hpp"
+
+using offset = ::gpiod::line::offset;
+using value = ::gpiod::line::value;
+using direction = ::gpiod::line::direction;
+using edge = ::gpiod::line::edge;
+using bias = ::gpiod::line::bias;
+using drive = ::gpiod::line::drive;
+using clock_type = ::gpiod::line::clock;
+using offsets = ::gpiod::line::offsets;
+using values = ::gpiod::line::values;
+using value_mapping = ::gpiod::line::value_mapping;
+using value_mappings = ::gpiod::line::value_mappings;
+
+namespace {
+
+TEST_CASE("stream insertion operators for types in gpiod::line work", "[line]")
+{
+       SECTION("offset")
+       {
+               offset off = 4;
+
+               REQUIRE_THAT(off, stringify_matcher<offset>("4"));
+       }
+
+       SECTION("value")
+       {
+               auto active = value::ACTIVE;
+               auto inactive = value::INACTIVE;
+
+               REQUIRE_THAT(active, stringify_matcher<value>("ACTIVE"));
+               REQUIRE_THAT(inactive, stringify_matcher<value>("INACTIVE"));
+       }
+
+       SECTION("direction")
+       {
+               auto input = direction::INPUT;
+               auto output = direction::OUTPUT;
+               auto as_is = direction::AS_IS;
+
+               REQUIRE_THAT(input, stringify_matcher<direction>("INPUT"));
+               REQUIRE_THAT(output, stringify_matcher<direction>("OUTPUT"));
+               REQUIRE_THAT(as_is, stringify_matcher<direction>("AS_IS"));
+       }
+
+       SECTION("edge")
+       {
+               auto rising = edge::RISING;
+               auto falling = edge::FALLING;
+               auto both = edge::BOTH;
+               auto none = edge::NONE;
+
+               REQUIRE_THAT(rising, stringify_matcher<edge>("RISING_EDGE"));
+               REQUIRE_THAT(falling, stringify_matcher<edge>("FALLING_EDGE"));
+               REQUIRE_THAT(both, stringify_matcher<edge>("BOTH_EDGES"));
+               REQUIRE_THAT(none, stringify_matcher<edge>("NONE"));
+       }
+
+       SECTION("bias")
+       {
+               auto pull_up = bias::PULL_UP;
+               auto pull_down = bias::PULL_DOWN;
+               auto disabled = bias::DISABLED;
+               auto as_is = bias::AS_IS;
+               auto unknown = bias::UNKNOWN;
+
+               REQUIRE_THAT(pull_up, stringify_matcher<bias>("PULL_UP"));
+               REQUIRE_THAT(pull_down, stringify_matcher<bias>("PULL_DOWN"));
+               REQUIRE_THAT(disabled, stringify_matcher<bias>("DISABLED"));
+               REQUIRE_THAT(as_is, stringify_matcher<bias>("AS_IS"));
+               REQUIRE_THAT(unknown, stringify_matcher<bias>("UNKNOWN"));
+       }
+
+       SECTION("drive")
+       {
+               auto push_pull = drive::PUSH_PULL;
+               auto open_drain = drive::OPEN_DRAIN;
+               auto open_source = drive::OPEN_SOURCE;
+
+               REQUIRE_THAT(push_pull, stringify_matcher<drive>("PUSH_PULL"));
+               REQUIRE_THAT(open_drain, stringify_matcher<drive>("OPEN_DRAIN"));
+               REQUIRE_THAT(open_source, stringify_matcher<drive>("OPEN_SOURCE"));
+       }
+
+       SECTION("clock")
+       {
+               auto monotonic = clock_type::MONOTONIC;
+               auto realtime = clock_type::REALTIME;
+               auto hte = clock_type::HTE;
+
+               REQUIRE_THAT(monotonic, stringify_matcher<clock_type>("MONOTONIC"));
+               REQUIRE_THAT(realtime, stringify_matcher<clock_type>("REALTIME"));
+               REQUIRE_THAT(hte, stringify_matcher<clock_type>("HTE"));
+       }
+
+       SECTION("offsets")
+       {
+               offsets offs = { 2, 5, 3, 9, 8, 7 };
+
+               REQUIRE_THAT(offs, stringify_matcher<offsets>("gpiod::offsets(2, 5, 3, 9, 8, 7)"));
+       }
+
+       SECTION("values")
+       {
+               values vals = {
+                       value::ACTIVE,
+                       value::INACTIVE,
+                       value::ACTIVE,
+                       value::ACTIVE,
+                       value::INACTIVE
+               };
+
+               REQUIRE_THAT(vals,
+                            stringify_matcher<values>("gpiod::values(ACTIVE, INACTIVE, ACTIVE, ACTIVE, INACTIVE)"));
+       }
+
+       SECTION("value_mapping")
+       {
+               value_mapping val = { 4, value::ACTIVE };
+
+               REQUIRE_THAT(val, stringify_matcher<value_mapping>("gpiod::value_mapping(4: ACTIVE)"));
+       }
+
+       SECTION("value_mappings")
+       {
+               value_mappings vals = { { 0, value::ACTIVE }, { 4, value::INACTIVE }, { 8, value::ACTIVE } };
+
+               REQUIRE_THAT(vals, stringify_matcher<value_mappings>(
+                       "gpiod::value_mappings(gpiod::value_mapping(0: ACTIVE), gpiod::value_mapping(4: INACTIVE), gpiod::value_mapping(8: ACTIVE))"));
+       }
+}
+
+} /* namespace */
diff --git a/bindings/cxx/tests/tests-misc.cpp b/bindings/cxx/tests/tests-misc.cpp
new file mode 100644 (file)
index 0000000..f06dc39
--- /dev/null
@@ -0,0 +1,78 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+// SPDX-FileCopyrightText: 2021-2022 Bartosz Golaszewski <brgl@bgdev.pl>
+
+#include <catch2/catch.hpp>
+#include <filesystem>
+#include <gpiod.hpp>
+#include <string>
+#include <regex>
+#include <unistd.h>
+
+#include "gpiosim.hpp"
+#include "helpers.hpp"
+
+using ::gpiosim::make_sim;
+
+namespace {
+
+class symlink_guard
+{
+public:
+       symlink_guard(const ::std::filesystem::path& target,
+                     const ::std::filesystem::path& link)
+               : _m_link(link)
+       {
+               ::std::filesystem::create_symlink(target, this->_m_link);
+       }
+
+       ~symlink_guard()
+       {
+               ::std::filesystem::remove(this->_m_link);
+       }
+
+private:
+       ::std::filesystem::path _m_link;
+};
+
+TEST_CASE("is_gpiochip_device() works", "[misc][chip]")
+{
+       SECTION("is_gpiochip_device() returns false for /dev/null")
+       {
+               REQUIRE_FALSE(::gpiod::is_gpiochip_device("/dev/null"));
+       }
+
+       SECTION("is_gpiochip_device() returns false for nonexistent file")
+       {
+               REQUIRE_FALSE(::gpiod::is_gpiochip_device("/dev/nonexistent"));
+       }
+
+       SECTION("is_gpiochip_device() returns true for a GPIO chip")
+       {
+               auto sim = make_sim().build();
+
+               REQUIRE(::gpiod::is_gpiochip_device(sim.dev_path()));
+       }
+
+       SECTION("is_gpiochip_device() can resolve a symlink")
+       {
+               auto sim = make_sim().build();
+               ::std::string link("/tmp/gpiod-cxx-tmp-link.");
+
+               link += ::std::to_string(::getpid());
+
+               symlink_guard link_guard(sim.dev_path(), link);
+
+               REQUIRE(::gpiod::is_gpiochip_device(link));
+       }
+}
+
+TEST_CASE("api_version() returns a valid API version", "[misc]")
+{
+       SECTION("check api_version() format")
+       {
+               REQUIRE_THAT(::gpiod::api_version(),
+                            regex_matcher("^\\d+\\.\\d+(\\.\\d+|\\-devel|\\-rc\\d+)?$"));
+       }
+}
+
+} /* namespace */
diff --git a/bindings/cxx/tests/tests-request-config.cpp b/bindings/cxx/tests/tests-request-config.cpp
new file mode 100644 (file)
index 0000000..66eb748
--- /dev/null
@@ -0,0 +1,83 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+// SPDX-FileCopyrightText: 2021-2022 Bartosz Golaszewski <brgl@bgdev.pl>
+
+#include <catch2/catch.hpp>
+#include <cstddef>
+#include <gpiod.hpp>
+#include <string>
+#include <sstream>
+
+#include "helpers.hpp"
+
+using offsets = ::gpiod::line::offsets;
+
+namespace {
+
+TEST_CASE("request_config constructor works", "[request-config]")
+{
+       SECTION("no arguments")
+       {
+               ::gpiod::request_config cfg;
+
+               REQUIRE(cfg.consumer().empty());
+               REQUIRE(cfg.event_buffer_size() == 0);
+       }
+}
+
+TEST_CASE("request_config can be moved", "[request-config]")
+{
+       ::gpiod::request_config cfg;
+
+       cfg.set_consumer("foobar").set_event_buffer_size(64);
+
+       SECTION("move constructor works")
+       {
+               auto moved(::std::move(cfg));
+               REQUIRE_THAT(moved.consumer(), Catch::Equals("foobar"));
+               REQUIRE(moved.event_buffer_size() == 64);
+       }
+
+       SECTION("move assignment operator works")
+       {
+               ::gpiod::request_config moved;
+
+               moved = ::std::move(cfg);
+
+               REQUIRE_THAT(moved.consumer(), Catch::Equals("foobar"));
+               REQUIRE(moved.event_buffer_size() == 64);
+       }
+}
+
+TEST_CASE("request_config mutators work", "[request-config]")
+{
+       ::gpiod::request_config cfg;
+
+       SECTION("set consumer")
+       {
+               cfg.set_consumer("foobar");
+               REQUIRE_THAT(cfg.consumer(), Catch::Equals("foobar"));
+       }
+
+       SECTION("set event_buffer_size")
+       {
+               cfg.set_event_buffer_size(128);
+               REQUIRE(cfg.event_buffer_size() == 128);
+       }
+}
+
+TEST_CASE("request_config stream insertion operator works", "[request-config]")
+{
+       ::gpiod::request_config cfg;
+
+       cfg.set_consumer("foobar").set_event_buffer_size(32);
+
+       ::std::stringstream buf;
+
+       buf << cfg;
+
+       ::std::string expected("gpiod::request_config(consumer='foobar', event_buffer_size=32)");
+
+       REQUIRE_THAT(buf.str(), Catch::Equals(expected));
+}
+
+} /* namespace */
diff --git a/bindings/python/.gitignore b/bindings/python/.gitignore
new file mode 100644 (file)
index 0000000..fce31f5
--- /dev/null
@@ -0,0 +1,8 @@
+# SPDX-License-Identifier: GPL-2.0-or-later
+# SPDX-FileCopyrightText: 2017-2022 Bartosz Golaszewski <brgl@bgdev.pl>
+
+build/
+__pycache__/
+dist/
+*.egg-info/
+*.so
diff --git a/bindings/python/MANIFEST.in b/bindings/python/MANIFEST.in
new file mode 100644 (file)
index 0000000..efdfd18
--- /dev/null
@@ -0,0 +1,19 @@
+# SPDX-License-Identifier: CC0-1.0
+# SPDX-FileCopyrightText: 2023 Bartosz Golaszewski <bartosz.golaszewski@linaro.org>
+
+include setup.py
+include README.md
+include libgpiod-version.txt
+
+recursive-include gpiod *.py
+recursive-include tests *.py
+
+recursive-include gpiod/ext *.c
+recursive-include gpiod/ext *.h
+
+recursive-include tests/gpiosim *.c
+recursive-include tests/procname *.c
+
+recursive-include lib *.c
+recursive-include lib *.h
+recursive-include include *.h
diff --git a/bindings/python/Makefile.am b/bindings/python/Makefile.am
new file mode 100644 (file)
index 0000000..61d82c5
--- /dev/null
@@ -0,0 +1,36 @@
+# SPDX-License-Identifier: GPL-2.0-or-later
+# SPDX-FileCopyrightText: 2022 Bartosz Golaszewski <brgl@bgdev.pl>
+
+EXTRA_DIST = \
+       MANIFEST.in \
+       setup.py
+
+if WITH_TESTS
+
+BUILD_TESTS = 1
+
+endif
+
+all-local:
+       GPIOD_WITH_TESTS=$(BUILD_TESTS) \
+       $(PYTHON) setup.py build_ext --inplace \
+               --include-dirs=$(top_srcdir)/include/:$(top_srcdir)/tests/gpiosim/ \
+               --library-dirs=$(top_builddir)/lib/.libs/:$(top_srcdir)/tests/gpiosim/.libs/
+
+install-exec-local:
+       GPIOD_WITH_TESTS= \
+       $(PYTHON) setup.py install --root=$(if $(DESTDIR),$(DESTDIR),/) --prefix=$(prefix)
+
+SUBDIRS = gpiod
+
+if WITH_TESTS
+
+SUBDIRS += tests
+
+endif
+
+if WITH_EXAMPLES
+
+SUBDIRS += examples
+
+endif
diff --git a/bindings/python/README.md b/bindings/python/README.md
new file mode 100644 (file)
index 0000000..abb69da
--- /dev/null
@@ -0,0 +1,104 @@
+# SPDX-License-Identifier: CC-BY-SA-4.0
+# SPDX-FileCopyrightText: 2023 Phil Howard <phil@gadgetoid.com>
+
+# gpiod
+
+These are the official Python bindings for [libgpiod](https://git.kernel.org/pub/scm/libs/libgpiod/libgpiod.git/about/).
+
+The gpiod library has been vendored into this package for your convenience and
+this version of gpiod is independent from your system package.
+
+Binary wheels are not provided. The source package requires python3-dev.
+
+## Rationale
+
+The new character device interface guarantees all allocated resources are
+freed after closing the device file descriptor and adds several new features
+that are not present in the obsolete sysfs interface (like event polling,
+setting/reading multiple values at once or open-source and open-drain GPIOs).
+
+Unfortunately interacting with the linux device file can no longer be done
+using only standard command-line tools. This is the reason for creating a
+library encapsulating the cumbersome, ioctl-based kernel-userspace interaction
+in a set of convenient functions and opaque data structures.
+
+## Breaking Changes
+
+As of v2.0.2 we have replaced the unofficial, pure-Python "gpiod". The official
+gpiod is not backwards compatible.
+
+You should ensure you specify at least v2.0.2 for the official API. Versions
+1.5.4 and prior are the deprecated, unofficial, pure-Python bindings.
+
+## Installing
+
+You will need `python3-dev`, on Debian/Ubuntu you can install this with:
+
+```
+sudo apt install python3-dev
+```
+
+And then install gpiod with:
+
+```
+pip install gpiod
+```
+
+You can optionally depend upon your system gpiod by installing with:
+
+```
+LINK_SYSTEM_LIBGPIOD=1 pip install gpiod
+```
+
+If you still need the deprecated pure-Python bindings, install with:
+
+```
+pip install gpiod==1.5.4
+```
+
+## Examples
+
+Check a GPIO chip character device exists:
+
+```python
+import gpiod
+
+gpiod.is_gpiochip_device("/dev/gpiochip0")
+
+```
+
+Get information about a GPIO chip character device:
+
+```python
+import gpiod
+
+with gpiod.Chip("/dev/gpiochip0") as chip:
+    info = chip.get_info()
+    print(f"{info.name} [{info.label}] ({info.num_lines} lines)")
+```
+
+Blink an LED, or toggling a GPIO line:
+
+```python
+import time
+
+from gpiod.line import Direction, Value
+
+LINE = 5
+
+with gpiod.request_lines(
+    "/dev/gpiochip0",
+    consumer="blink-example",
+    config={
+        LINE: gpiod.LineSettings(
+            direction=Direction.OUTPUT, output_value=Value.ACTIVE
+        )
+    },
+) as request:
+    while True:
+        request.set_value(LINE, Value.ACTIVE)
+        time.sleep(1)
+        request.set_value(LINE, Value.INACTIVE)
+        time.sleep(1)
+```
+
diff --git a/bindings/python/examples/Makefile.am b/bindings/python/examples/Makefile.am
new file mode 100644 (file)
index 0000000..8852312
--- /dev/null
@@ -0,0 +1,17 @@
+# SPDX-License-Identifier: GPL-2.0-or-later
+# SPDX-FileCopyrightText: 2022 Bartosz Golaszewski <brgl@bgdev.pl>
+
+EXTRA_DIST = \
+       async_watch_line_value.py \
+       find_line_by_name.py \
+       get_chip_info.py \
+       get_line_info.py \
+       get_line_value.py \
+       get_multiple_line_values.py \
+       reconfigure_input_to_output.py \
+       toggle_line_value.py \
+       toggle_multiple_line_values.py \
+       watch_line_info.py \
+       watch_line_value.py \
+       watch_line_rising.py \
+       watch_multiple_line_values.py
diff --git a/bindings/python/examples/async_watch_line_value.py b/bindings/python/examples/async_watch_line_value.py
new file mode 100755 (executable)
index 0000000..1d6a184
--- /dev/null
@@ -0,0 +1,54 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: GPL-2.0-or-later
+# SPDX-FileCopyrightText: 2023 Kent Gibson <warthog618@gmail.com>
+
+"""Minimal example of asynchronously watching for edges on a single line."""
+
+import gpiod
+import select
+
+from datetime import timedelta
+from gpiod.line import Bias, Edge
+
+
+def edge_type_str(event):
+    if event.event_type is event.Type.RISING_EDGE:
+        return "Rising"
+    if event.event_type is event.Type.FALLING_EDGE:
+        return "Falling"
+    return "Unknown"
+
+
+def async_watch_line_value(chip_path, line_offset):
+    # Assume a button connecting the pin to ground,
+    # so pull it up and provide some debounce.
+    with gpiod.request_lines(
+        chip_path,
+        consumer="async-watch-line-value",
+        config={
+            line_offset: gpiod.LineSettings(
+                edge_detection=Edge.BOTH,
+                bias=Bias.PULL_UP,
+                debounce_period=timedelta(milliseconds=10),
+            )
+        },
+    ) as request:
+        poll = select.poll()
+        poll.register(request.fd, select.POLLIN)
+        while True:
+            # Other fds could be registered with the poll and be handled
+            # separately using the return value (fd, event) from poll()
+            poll.poll()
+            for event in request.read_edge_events():
+                print(
+                    "offset: {}  type: {:<7}  event #{}".format(
+                        event.line_offset, edge_type_str(event), event.line_seqno
+                    )
+                )
+
+
+if __name__ == "__main__":
+    try:
+        async_watch_line_value("/dev/gpiochip0", 5)
+    except OSError as ex:
+        print(ex, "\nCustomise the example configuration to suit your situation")
diff --git a/bindings/python/examples/find_line_by_name.py b/bindings/python/examples/find_line_by_name.py
new file mode 100755 (executable)
index 0000000..ac798a9
--- /dev/null
@@ -0,0 +1,37 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: GPL-2.0-or-later
+# SPDX-FileCopyrightText: 2023 Kent Gibson <warthog618@gmail.com>
+
+"""Minimal example of finding a line with the given name."""
+
+import gpiod
+import os
+
+
+def generate_gpio_chips():
+    for entry in os.scandir("/dev/"):
+        if gpiod.is_gpiochip_device(entry.path):
+            yield entry.path
+
+
+def find_line_by_name(line_name):
+    # Names are not guaranteed unique, so this finds the first line with
+    # the given name.
+    for path in generate_gpio_chips():
+        with gpiod.Chip(path) as chip:
+            try:
+                offset = chip.line_offset_from_id(line_name)
+                print("{}: {} {}".format(line_name, chip.get_info().name, offset))
+                return
+            except OSError:
+                # An OSError is raised if the name is not found.
+                continue
+
+    print("line '{}' not found".format(line_name))
+
+
+if __name__ == "__main__":
+    try:
+        find_line_by_name("GPIO19")
+    except OSError as ex:
+        print(ex, "\nCustomise the example configuration to suit your situation")
diff --git a/bindings/python/examples/get_chip_info.py b/bindings/python/examples/get_chip_info.py
new file mode 100755 (executable)
index 0000000..7dc76fb
--- /dev/null
@@ -0,0 +1,20 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: GPL-2.0-or-later
+# SPDX-FileCopyrightText: 2023 Kent Gibson <warthog618@gmail.com>
+
+"""Minimal example of reading the info for a chip."""
+
+import gpiod
+
+
+def get_chip_info(chip_path):
+    with gpiod.Chip(chip_path) as chip:
+        info = chip.get_info()
+        print("{} [{}] ({} lines)".format(info.name, info.label, info.num_lines))
+
+
+if __name__ == "__main__":
+    try:
+        get_chip_info("/dev/gpiochip0")
+    except OSError as ex:
+        print(ex, "\nCustomise the example configuration to suit your situation")
diff --git a/bindings/python/examples/get_line_info.py b/bindings/python/examples/get_line_info.py
new file mode 100755 (executable)
index 0000000..cd4ebcc
--- /dev/null
@@ -0,0 +1,29 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: GPL-2.0-or-later
+# SPDX-FileCopyrightText: 2023 Kent Gibson <warthog618@gmail.com>
+
+"""Minimal example of reading the info for a line."""
+
+import gpiod
+
+
+def get_line_info(chip_path, line_offset):
+    with gpiod.Chip(chip_path) as chip:
+        info = chip.get_line_info(line_offset)
+        is_input = info.direction == gpiod.line.Direction.INPUT
+        print(
+            "line {:>3}: {:>12} {:>12} {:>8} {:>10}".format(
+                info.offset,
+                info.name or "unnamed",
+                info.consumer or "unused",
+                "input" if is_input else "output",
+                "active-low" if info.active_low else "active-high",
+            )
+        )
+
+
+if __name__ == "__main__":
+    try:
+        get_line_info("/dev/gpiochip0", 3)
+    except OSError as ex:
+        print(ex, "\nCustomise the example configuration to suit your situation")
diff --git a/bindings/python/examples/get_line_value.py b/bindings/python/examples/get_line_value.py
new file mode 100755 (executable)
index 0000000..f3ca13b
--- /dev/null
@@ -0,0 +1,26 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: GPL-2.0-or-later
+# SPDX-FileCopyrightText: 2023 Kent Gibson <warthog618@gmail.com>
+
+"""Minimal example of reading a single line."""
+
+import gpiod
+
+from gpiod.line import Direction
+
+
+def get_line_value(chip_path, line_offset):
+    with gpiod.request_lines(
+        chip_path,
+        consumer="get-line-value",
+        config={line_offset: gpiod.LineSettings(direction=Direction.INPUT)},
+    ) as request:
+        value = request.get_value(line_offset)
+        print("{}={}".format(line_offset, value))
+
+
+if __name__ == "__main__":
+    try:
+        get_line_value("/dev/gpiochip0", 5)
+    except OSError as ex:
+        print(ex, "\nCustomise the example configuration to suit your situation")
diff --git a/bindings/python/examples/get_multiple_line_values.py b/bindings/python/examples/get_multiple_line_values.py
new file mode 100755 (executable)
index 0000000..46cf0b2
--- /dev/null
@@ -0,0 +1,29 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: GPL-2.0-or-later
+# SPDX-FileCopyrightText: 2023 Kent Gibson <warthog618@gmail.com>
+
+"""Minimal example of reading multiple lines."""
+
+import gpiod
+
+from gpiod.line import Direction
+
+
+def get_multiple_line_values(chip_path, line_offsets):
+    with gpiod.request_lines(
+        chip_path,
+        consumer="get-multiple-line-values",
+        config={tuple(line_offsets): gpiod.LineSettings(direction=Direction.INPUT)},
+    ) as request:
+        vals = request.get_values()
+
+        for offset, val in zip(line_offsets, vals):
+            print("{}={} ".format(offset, val), end="")
+        print()
+
+
+if __name__ == "__main__":
+    try:
+        get_multiple_line_values("/dev/gpiochip0", [5, 3, 7])
+    except OSError as ex:
+        print(ex, "\nCustomise the example configuration to suit your situation")
diff --git a/bindings/python/examples/reconfigure_input_to_output.py b/bindings/python/examples/reconfigure_input_to_output.py
new file mode 100755 (executable)
index 0000000..0f2e358
--- /dev/null
@@ -0,0 +1,39 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: GPL-2.0-or-later
+# SPDX-FileCopyrightText: 2023 Kent Gibson <warthog618@gmail.com>
+
+"""Example of a bi-directional line requested as input and then switched to output."""
+
+import gpiod
+
+from gpiod.line import Direction, Value
+
+
+def reconfigure_input_to_output(chip_path, line_offset):
+    # request the line initially as an input
+    with gpiod.request_lines(
+        chip_path,
+        consumer="reconfigure-input-to-output",
+        config={line_offset: gpiod.LineSettings(direction=Direction.INPUT)},
+    ) as request:
+        # read the current line value
+        value = request.get_value(line_offset)
+        print("{}={} (input)".format(line_offset, value))
+        # switch the line to an output and drive it low
+        request.reconfigure_lines(
+            config={
+                line_offset: gpiod.LineSettings(
+                    direction=Direction.OUTPUT, output_value=Value.INACTIVE
+                )
+            }
+        )
+        # report the current driven value
+        value = request.get_value(line_offset)
+        print("{}={} (output)".format(line_offset, value))
+
+
+if __name__ == "__main__":
+    try:
+        reconfigure_input_to_output("/dev/gpiochip0", 5)
+    except OSError as ex:
+        print(ex, "\nCustomise the example configuration to suit your situation")
diff --git a/bindings/python/examples/toggle_line_value.py b/bindings/python/examples/toggle_line_value.py
new file mode 100755 (executable)
index 0000000..e0de8fb
--- /dev/null
@@ -0,0 +1,43 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: GPL-2.0-or-later
+# SPDX-FileCopyrightText: 2023 Kent Gibson <warthog618@gmail.com>
+
+"""Minimal example of toggling a single line."""
+
+import gpiod
+import time
+
+from gpiod.line import Direction, Value
+
+
+def toggle_value(value):
+    if value == Value.INACTIVE:
+        return Value.ACTIVE
+    return Value.INACTIVE
+
+
+def toggle_line_value(chip_path, line_offset):
+    value_str = {Value.ACTIVE: "Active", Value.INACTIVE: "Inactive"}
+    value = Value.ACTIVE
+
+    with gpiod.request_lines(
+        chip_path,
+        consumer="toggle-line-value",
+        config={
+            line_offset: gpiod.LineSettings(
+                direction=Direction.OUTPUT, output_value=value
+            )
+        },
+    ) as request:
+        while True:
+            print("{}={}".format(line_offset, value_str[value]))
+            time.sleep(1)
+            value = toggle_value(value)
+            request.set_value(line_offset, value)
+
+
+if __name__ == "__main__":
+    try:
+        toggle_line_value("/dev/gpiochip0", 5)
+    except OSError as ex:
+        print(ex, "\nCustomise the example configuration to suit your situation")
diff --git a/bindings/python/examples/toggle_multiple_line_values.py b/bindings/python/examples/toggle_multiple_line_values.py
new file mode 100755 (executable)
index 0000000..12b968d
--- /dev/null
@@ -0,0 +1,47 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: GPL-2.0-or-later
+# SPDX-FileCopyrightText: 2023 Kent Gibson <warthog618@gmail.com>
+
+"""Minimal example of toggling multiple lines."""
+
+import gpiod
+import time
+
+from gpiod.line import Direction, Value
+
+
+def toggle_value(value):
+    if value == Value.INACTIVE:
+        return Value.ACTIVE
+    return Value.INACTIVE
+
+
+def toggle_multiple_line_values(chip_path, line_values):
+    value_str = {Value.ACTIVE: "Active", Value.INACTIVE: "Inactive"}
+
+    request = gpiod.request_lines(
+        chip_path,
+        consumer="toggle-multiple-line-values",
+        config={
+            tuple(line_values.keys()): gpiod.LineSettings(direction=Direction.OUTPUT)
+        },
+        output_values=line_values,
+    )
+
+    while True:
+        print(
+            " ".join("{}={}".format(l, value_str[v]) for (l, v) in line_values.items())
+        )
+        time.sleep(1)
+        for l, v in line_values.items():
+            line_values[l] = toggle_value(v)
+        request.set_values(line_values)
+
+
+if __name__ == "__main__":
+    try:
+        toggle_multiple_line_values(
+            "/dev/gpiochip0", {5: Value.ACTIVE, 3: Value.ACTIVE, 7: Value.INACTIVE}
+        )
+    except OSError as ex:
+        print(ex, "\nCustomise the example configuration to suit your situation")
diff --git a/bindings/python/examples/watch_line_info.py b/bindings/python/examples/watch_line_info.py
new file mode 100755 (executable)
index 0000000..e49a669
--- /dev/null
@@ -0,0 +1,23 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: GPL-2.0-or-later
+# SPDX-FileCopyrightText: 2023 Kent Gibson <warthog618@gmail.com>
+
+"""Minimal example of watching for info changes on particular lines."""
+
+import gpiod
+
+
+def watch_line_info(chip_path, line_offsets):
+    with gpiod.Chip(chip_path) as chip:
+        for offset in line_offsets:
+            chip.watch_line_info(offset)
+
+        while True:
+            print(chip.read_info_event())
+
+
+if __name__ == "__main__":
+    try:
+        watch_line_info("/dev/gpiochip0", [5, 3, 7])
+    except OSError as ex:
+        print(ex, "\nCustomise the example configuration to suit your situation")
diff --git a/bindings/python/examples/watch_line_rising.py b/bindings/python/examples/watch_line_rising.py
new file mode 100755 (executable)
index 0000000..7b1c6b0
--- /dev/null
@@ -0,0 +1,32 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: GPL-2.0-or-later
+# SPDX-FileCopyrightText: 2023 Kent Gibson <warthog618@gmail.com>
+
+"""Minimal example of watching for rising edges on a single line."""
+
+import gpiod
+
+from gpiod.line import Edge
+
+
+def watch_line_rising(chip_path, line_offset):
+    with gpiod.request_lines(
+        chip_path,
+        consumer="watch-line-rising",
+        config={line_offset: gpiod.LineSettings(edge_detection=Edge.RISING)},
+    ) as request:
+        while True:
+            # Blocks until at least one event is available
+            for event in request.read_edge_events():
+                print(
+                    "line: {}  type: Rising   event #{}".format(
+                        event.line_offset, event.line_seqno
+                    )
+                )
+
+
+if __name__ == "__main__":
+    try:
+        watch_line_rising("/dev/gpiochip0", 5)
+    except OSError as ex:
+        print(ex, "\nCustomise the example configuration to suit your situation")
diff --git a/bindings/python/examples/watch_line_value.py b/bindings/python/examples/watch_line_value.py
new file mode 100755 (executable)
index 0000000..171a67c
--- /dev/null
@@ -0,0 +1,49 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: GPL-2.0-or-later
+# SPDX-FileCopyrightText: 2023 Kent Gibson <warthog618@gmail.com>
+
+"""Minimal example of watching for edges on a single line."""
+
+import gpiod
+
+from datetime import timedelta
+from gpiod.line import Bias, Edge
+
+
+def edge_type_str(event):
+    if event.event_type is event.Type.RISING_EDGE:
+        return "Rising"
+    if event.event_type is event.Type.FALLING_EDGE:
+        return "Falling"
+    return "Unknown"
+
+
+def watch_line_value(chip_path, line_offset):
+    # Assume a button connecting the pin to ground,
+    # so pull it up and provide some debounce.
+    with gpiod.request_lines(
+        chip_path,
+        consumer="watch-line-value",
+        config={
+            line_offset: gpiod.LineSettings(
+                edge_detection=Edge.BOTH,
+                bias=Bias.PULL_UP,
+                debounce_period=timedelta(milliseconds=10),
+            )
+        },
+    ) as request:
+        while True:
+            # Blocks until at least one event is available
+            for event in request.read_edge_events():
+                print(
+                    "line: {}  type: {:<7}  event #{}".format(
+                        event.line_offset, edge_type_str(event), event.line_seqno
+                    )
+                )
+
+
+if __name__ == "__main__":
+    try:
+        watch_line_value("/dev/gpiochip0", 5)
+    except OSError as ex:
+        print(ex, "\nCustomise the example configuration to suit your situation")
diff --git a/bindings/python/examples/watch_multiple_line_values.py b/bindings/python/examples/watch_multiple_line_values.py
new file mode 100755 (executable)
index 0000000..5906b7d
--- /dev/null
@@ -0,0 +1,42 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: GPL-2.0-or-later
+# SPDX-FileCopyrightText: 2023 Kent Gibson <warthog618@gmail.com>
+
+"""Minimal example of watching for edges on multiple lines."""
+
+import gpiod
+
+from gpiod.line import Edge
+
+
+def edge_type_str(event):
+    if event.event_type is event.Type.RISING_EDGE:
+        return "Rising"
+    if event.event_type is event.Type.FALLING_EDGE:
+        return "Falling"
+    return "Unknown"
+
+
+def watch_multiple_line_values(chip_path, line_offsets):
+    with gpiod.request_lines(
+        chip_path,
+        consumer="watch-multiple-line-values",
+        config={tuple(line_offsets): gpiod.LineSettings(edge_detection=Edge.BOTH)},
+    ) as request:
+        while True:
+            for event in request.read_edge_events():
+                print(
+                    "offset: {}  type: {:<7}  event #{}  line event #{}".format(
+                        event.line_offset,
+                        edge_type_str(event),
+                        event.global_seqno,
+                        event.line_seqno,
+                    )
+                )
+
+
+if __name__ == "__main__":
+    try:
+        watch_multiple_line_values("/dev/gpiochip0", [5, 3, 7])
+    except OSError as ex:
+        print(ex, "\nCustomise the example configuration to suit your situation")
diff --git a/bindings/python/gpiod/Makefile.am b/bindings/python/gpiod/Makefile.am
new file mode 100644 (file)
index 0000000..daf7bae
--- /dev/null
@@ -0,0 +1,18 @@
+# SPDX-License-Identifier: LGPL-2.1-or-later
+# SPDX-FileCopyrightText: 2022 Bartosz Golaszewski <brgl@bgdev.pl>
+
+SUBDIRS = ext
+
+EXTRA_DIST = \
+       chip_info.py \
+       chip.py \
+       edge_event.py \
+       exception.py \
+       info_event.py \
+       __init__.py \
+       internal.py \
+       line_info.py \
+       line.py \
+       line_request.py \
+       line_settings.py \
+       version.py
diff --git a/bindings/python/gpiod/__init__.py b/bindings/python/gpiod/__init__.py
new file mode 100644 (file)
index 0000000..9cbb8df
--- /dev/null
@@ -0,0 +1,54 @@
+# SPDX-License-Identifier: LGPL-2.1-or-later
+# SPDX-FileCopyrightText: 2022 Bartosz Golaszewski <brgl@bgdev.pl>
+
+"""
+Python bindings for libgpiod.
+
+This module wraps the native C API of libgpiod in a set of python classes.
+"""
+
+from . import _ext
+from . import line
+from .chip import Chip
+from .chip_info import ChipInfo
+from .edge_event import EdgeEvent
+from .exception import ChipClosedError, RequestReleasedError
+from .info_event import InfoEvent
+from .line_request import LineRequest
+from .line_settings import LineSettings
+from .version import __version__
+
+api_version = _ext.api_version
+
+
+def is_gpiochip_device(path: str) -> bool:
+    """
+    Check if the file pointed to by path is a GPIO chip character device.
+
+    Args:
+      path
+        Path to the file that should be checked.
+
+    Returns:
+      Returns true if so, False otherwise.
+    """
+    return _ext.is_gpiochip_device(path)
+
+
+def request_lines(path: str, *args, **kwargs) -> LineRequest:
+    """
+    Open a GPIO chip pointed to by 'path', request lines according to the
+    configuration arguments, close the chip and return the request object.
+
+    Args:
+      path
+        Path to the GPIO character device file.
+      *args
+      **kwargs
+        See Chip.request_lines() for configuration arguments.
+
+    Returns:
+      Returns a new LineRequest object.
+    """
+    with Chip(path) as chip:
+        return chip.request_lines(*args, **kwargs)
diff --git a/bindings/python/gpiod/chip.py b/bindings/python/gpiod/chip.py
new file mode 100644 (file)
index 0000000..b3d8e61
--- /dev/null
@@ -0,0 +1,363 @@
+# SPDX-License-Identifier: LGPL-2.1-or-later
+# SPDX-FileCopyrightText: 2022 Bartosz Golaszewski <brgl@bgdev.pl>
+
+from . import _ext
+from .chip_info import ChipInfo
+from .exception import ChipClosedError
+from .info_event import InfoEvent
+from .internal import poll_fd
+from .line import Value
+from .line_info import LineInfo
+from .line_settings import LineSettings, _line_settings_to_ext
+from .line_request import LineRequest
+from collections import Counter
+from collections.abc import Iterable
+from datetime import timedelta
+from errno import ENOENT
+from select import select
+from typing import Union, Optional
+
+__all__ = "Chip"
+
+
+class Chip:
+    """
+    Represents a GPIO chip.
+
+    Chip object manages all resources associated with the GPIO chip it represents.
+
+    The gpiochip device file is opened during the object's construction. The Chip
+    object's constructor takes the path to the GPIO chip device file
+    as the only argument.
+
+    Callers must close the chip by calling the close() method when it's no longer
+    used.
+
+    Example:
+
+        chip = gpiod.Chip(\"/dev/gpiochip0\")
+        do_something(chip)
+        chip.close()
+
+    The gpiod.Chip class also supports controlled execution ('with' statement).
+
+    Example:
+
+        with gpiod.Chip(path="/dev/gpiochip0") as chip:
+            do_something(chip)
+    """
+
+    def __init__(self, path: str):
+        """
+        Open a GPIO device.
+
+        Args:
+          path:
+            Path to the GPIO character device file.
+        """
+        self._chip = _ext.Chip(path)
+        self._info = None
+
+    def __bool__(self) -> bool:
+        """
+        Boolean conversion for GPIO chips.
+
+        Returns:
+          True if the chip is open and False if it's closed.
+        """
+        return True if self._chip else False
+
+    def __enter__(self):
+        """
+        Controlled execution enter callback.
+        """
+        self._check_closed()
+        return self
+
+    def __exit__(self, exc_type, exc_value, traceback) -> None:
+        """
+        Controlled execution exit callback.
+        """
+        self.close()
+
+    def _check_closed(self) -> None:
+        if not self._chip:
+            raise ChipClosedError()
+
+    def close(self) -> None:
+        """
+        Close the associated GPIO chip descriptor. The chip object must no
+        longer be used after this method is called.
+        """
+        self._check_closed()
+        self._chip.close()
+        self._chip = None
+
+    def get_info(self) -> ChipInfo:
+        """
+        Get the information about the chip.
+
+        Returns:
+          New gpiod.ChipInfo object.
+        """
+        self._check_closed()
+
+        if not self._info:
+            self._info = self._chip.get_info()
+
+        return self._info
+
+    def line_offset_from_id(self, id: Union[str, int]) -> int:
+        """
+        Map a line's identifier to its offset within the chip.
+
+        Args:
+          id:
+            Name of the GPIO line, its offset as a string or its offset as an
+            integer.
+
+        Returns:
+          If id is an integer - it's returned as is (unless it's out of range
+          for this chip). If it's a string, the method tries to interpret it as
+          the name of the line first and tries too perform a name lookup within
+          the chip. If it fails, it tries to convert the string to an integer
+          and check if it represents a valid offset within the chip and if
+          so - returns it.
+        """
+        self._check_closed()
+
+        if not isinstance(id, int):
+            try:
+                return self._chip.line_offset_from_id(id)
+            except OSError as ex:
+                if ex.errno == ENOENT:
+                    try:
+                        offset = int(id)
+                    except ValueError:
+                        raise ex
+                else:
+                    raise ex
+        else:
+            offset = id
+
+        if offset >= self.get_info().num_lines:
+            raise ValueError("line offset of out range")
+
+        return offset
+
+    def _get_line_info(self, line: Union[int, str], watch: bool) -> LineInfo:
+        self._check_closed()
+        return self._chip.get_line_info(self.line_offset_from_id(line), watch)
+
+    def get_line_info(self, line: Union[int, str]) -> LineInfo:
+        """
+        Get the snapshot of information about the line at given offset.
+
+        Args:
+          line:
+            Offset or name of the GPIO line to get information for.
+
+        Returns:
+          New LineInfo object.
+        """
+        return self._get_line_info(line, watch=False)
+
+    def watch_line_info(self, line: Union[int, str]) -> LineInfo:
+        """
+        Get the snapshot of information about the line at given offset and
+        start watching it for future changes.
+
+        Args:
+          line:
+            Offset or name of the GPIO line to get information for.
+
+        Returns:
+          New gpiod.LineInfo object.
+        """
+        return self._get_line_info(line, watch=True)
+
+    def unwatch_line_info(self, line: Union[int, str]) -> None:
+        """
+        Stop watching a line for status changes.
+
+        Args:
+          line:
+            Offset or name of the line to stop watching.
+        """
+        self._check_closed()
+        return self._chip.unwatch_line_info(self.line_offset_from_id(line))
+
+    def wait_info_event(
+        self, timeout: Optional[Union[timedelta, float]] = None
+    ) -> bool:
+        """
+        Wait for line status change events on any of the watched lines on the
+        chip.
+
+        Args:
+          timeout:
+            Wait time limit represented as either a datetime.timedelta object
+            or the number of seconds stored in a float. If set to 0, the
+            method returns immediately, if set to None it blocks indefinitely.
+
+        Returns:
+          True if an info event is ready to be read from the chip, False if the
+          wait timed out without any events.
+        """
+        self._check_closed()
+
+        return poll_fd(self.fd, timeout)
+
+    def read_info_event(self) -> InfoEvent:
+        """
+        Read a single line status change event from the chip.
+
+        Returns:
+          New gpiod.InfoEvent object.
+
+        Note:
+          This function may block if there are no available events in the queue.
+        """
+        self._check_closed()
+        return self._chip.read_info_event()
+
+    def request_lines(
+        self,
+        config: dict[tuple[Union[int, str]], Optional[LineSettings]],
+        consumer: Optional[str] = None,
+        event_buffer_size: Optional[int] = None,
+        output_values: Optional[dict[tuple[Union[int, str]], Value]] = None,
+    ) -> LineRequest:
+        """
+        Request a set of lines for exclusive usage.
+
+        Args:
+          config:
+            Dictionary mapping offsets or names (or tuples thereof) to
+            LineSettings. If None is passed as the value of the mapping,
+            default settings are used.
+          consumer:
+            Consumer string to use for this request.
+          event_buffer_size:
+            Size of the kernel edge event buffer to configure for this request.
+          output_values:
+            Dictionary mapping offsets or names to line.Value. This can be used
+            to set the desired output values globally while reusing LineSettings
+            for more lines.
+
+        Returns:
+          New LineRequest object.
+        """
+        self._check_closed()
+
+        line_cfg = _ext.LineConfig()
+
+        # Sanitize lines - don't allow offset repeatitions or offset-name conflicts.
+        for offset, count in Counter(
+            [
+                self.line_offset_from_id(line)
+                for line in (
+                    lambda t: [
+                        j for i in (t) for j in (i if isinstance(i, tuple) else (i,))
+                    ]
+                )(tuple(config.keys()))
+            ]
+        ).items():
+            if count != 1:
+                raise ValueError(
+                    "line must be configured exactly once - offset {} repeats".format(
+                        offset
+                    )
+                )
+
+        # If we have global output values - map line names to offsets
+        if output_values:
+            mapped_output_values = {
+                self.line_offset_from_id(line): value
+                for line, value in output_values.items()
+            }
+        else:
+            mapped_output_values = None
+
+        for lines, settings in config.items():
+            offsets = list()
+            name_map = dict()
+            offset_map = dict()
+            global_output_values = list()
+
+            if isinstance(lines, int) or isinstance(lines, str):
+                lines = (lines,)
+
+            for line in lines:
+                offset = self.line_offset_from_id(line)
+                offsets.append(offset)
+
+                # If there's a global output value for this offset, store it in the
+                # list for later.
+                if mapped_output_values:
+                    global_output_values.append(
+                        mapped_output_values[offset]
+                        if offset in mapped_output_values
+                        else Value.INACTIVE
+                    )
+
+                if isinstance(line, str):
+                    name_map[line] = offset
+                    offset_map[offset] = line
+
+            line_cfg.add_line_settings(
+                offsets, _line_settings_to_ext(settings or LineSettings())
+            )
+
+        if len(global_output_values):
+            line_cfg.set_output_values(global_output_values)
+
+        req_internal = self._chip.request_lines(line_cfg, consumer, event_buffer_size)
+        request = LineRequest(req_internal)
+
+        request._chip_name = req_internal.chip_name
+        request._offsets = req_internal.offsets
+        request._name_map = name_map
+        request._offset_map = offset_map
+
+        request._lines = [
+            offset_map[off] if off in offset_map else off for off in request.offsets
+        ]
+
+        return request
+
+    def __repr__(self) -> str:
+        """
+        Return a string that can be used to re-create this chip object.
+        """
+        if not self._chip:
+            return "<Chip CLOSED>"
+
+        return 'Chip("{}")'.format(self.path)
+
+    def __str__(self) -> str:
+        """
+        Return a user-friendly, human-readable description of this chip.
+        """
+        if not self._chip:
+            return "<Chip CLOSED>"
+
+        return '<Chip path="{}" fd={} info={}>'.format(
+            self.path, self.fd, self.get_info()
+        )
+
+    @property
+    def path(self) -> str:
+        """
+        Filesystem path used to open this chip.
+        """
+        self._check_closed()
+        return self._chip.path
+
+    @property
+    def fd(self) -> int:
+        """
+        File descriptor associated with this chip.
+        """
+        self._check_closed()
+        return self._chip.fd
diff --git a/bindings/python/gpiod/chip_info.py b/bindings/python/gpiod/chip_info.py
new file mode 100644 (file)
index 0000000..92b5e6f
--- /dev/null
@@ -0,0 +1,23 @@
+# SPDX-License-Identifier: LGPL-2.1-or-later
+# SPDX-FileCopyrightText: 2022 Bartosz Golaszewski <brgl@bgdev.pl>
+
+
+from dataclasses import dataclass
+
+__all__ = "ChipInfo"
+
+
+@dataclass(frozen=True, repr=False)
+class ChipInfo:
+    """
+    Snapshot of a chip's status.
+    """
+
+    name: str
+    label: str
+    num_lines: int
+
+    def __str__(self):
+        return '<ChipInfo name="{}" label="{}" num_lines={}>'.format(
+            self.name, self.label, self.num_lines
+        )
diff --git a/bindings/python/gpiod/edge_event.py b/bindings/python/gpiod/edge_event.py
new file mode 100644 (file)
index 0000000..bf258c1
--- /dev/null
@@ -0,0 +1,48 @@
+# SPDX-License-Identifier: LGPL-2.1-or-later
+# SPDX-FileCopyrightText: 2022 Bartosz Golaszewski <brgl@bgdev.pl>
+
+from . import _ext
+from dataclasses import dataclass
+from enum import Enum
+
+__all__ = "EdgeEvent"
+
+
+@dataclass(frozen=True, init=False, repr=False)
+class EdgeEvent:
+    """
+    Immutable object containing data about a single edge event.
+    """
+
+    class Type(Enum):
+        RISING_EDGE = _ext.EDGE_EVENT_TYPE_RISING
+        FALLING_EDGE = _ext.EDGE_EVENT_TYPE_FALLING
+
+    event_type: Type
+    timestamp_ns: int
+    line_offset: int
+    global_seqno: int
+    line_seqno: int
+
+    def __init__(
+        self,
+        event_type: int,
+        timestamp_ns: int,
+        line_offset: int,
+        global_seqno: int,
+        line_seqno: int,
+    ):
+        object.__setattr__(self, "event_type", EdgeEvent.Type(event_type))
+        object.__setattr__(self, "timestamp_ns", timestamp_ns)
+        object.__setattr__(self, "line_offset", line_offset)
+        object.__setattr__(self, "global_seqno", global_seqno)
+        object.__setattr__(self, "line_seqno", line_seqno)
+
+    def __str__(self):
+        return "<EdgeEvent type={} timestamp_ns={} line_offset={} global_seqno={} line_seqno={}>".format(
+            self.event_type,
+            self.timestamp_ns,
+            self.line_offset,
+            self.global_seqno,
+            self.line_seqno,
+        )
diff --git a/bindings/python/gpiod/exception.py b/bindings/python/gpiod/exception.py
new file mode 100644 (file)
index 0000000..f9a83c2
--- /dev/null
@@ -0,0 +1,22 @@
+# SPDX-License-Identifier: LGPL-2.1-or-later
+# SPDX-FileCopyrightText: 2022 Bartosz Golaszewski <brgl@bgdev.pl>
+
+__all__ = ["ChipClosedError", "RequestReleasedError"]
+
+
+class ChipClosedError(Exception):
+    """
+    Error raised when an already closed chip is used.
+    """
+
+    def __init__(self):
+        super().__init__("I/O operation on closed chip")
+
+
+class RequestReleasedError(Exception):
+    """
+    Error raised when a released request is used.
+    """
+
+    def __init__(self):
+        super().__init__("GPIO lines have been released")
diff --git a/bindings/python/gpiod/ext/Makefile.am b/bindings/python/gpiod/ext/Makefile.am
new file mode 100644 (file)
index 0000000..9c81b17
--- /dev/null
@@ -0,0 +1,11 @@
+# SPDX-License-Identifier: LGPL-2.1-or-later
+# SPDX-FileCopyrightText: 2022 Bartosz Golaszewski <brgl@bgdev.pl>
+
+EXTRA_DIST = \
+       chip.c \
+       common.c \
+       internal.h \
+       line-config.c \
+       line-settings.c \
+       module.c \
+       request.c
diff --git a/bindings/python/gpiod/ext/chip.c b/bindings/python/gpiod/ext/chip.c
new file mode 100644 (file)
index 0000000..e8eaad8
--- /dev/null
@@ -0,0 +1,341 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// SPDX-FileCopyrightText: 2022 Bartosz Golaszewski <brgl@bgdev.pl>
+
+#include "internal.h"
+
+typedef struct {
+       PyObject_HEAD;
+       struct gpiod_chip *chip;
+} chip_object;
+
+static int
+chip_init(chip_object *self, PyObject *args, PyObject *Py_UNUSED(ignored))
+{
+       struct gpiod_chip *chip;
+       char *path;
+       int ret;
+
+       ret = PyArg_ParseTuple(args, "s", &path);
+       if (!ret)
+               return -1;
+
+       Py_BEGIN_ALLOW_THREADS;
+       chip = gpiod_chip_open(path);
+       Py_END_ALLOW_THREADS;
+       if (!chip) {
+               Py_gpiod_SetErrFromErrno();
+               return -1;
+       }
+
+       self->chip = chip;
+
+       return 0;
+}
+
+static void chip_finalize(chip_object *self)
+{
+       if (self->chip)
+               PyObject_CallMethod((PyObject *)self, "close", "");
+}
+
+static PyObject *chip_path(chip_object *self, void *Py_UNUSED(ignored))
+{
+       return PyUnicode_FromString(gpiod_chip_get_path(self->chip));
+}
+
+static PyObject *chip_fd(chip_object *self, void *Py_UNUSED(ignored))
+{
+       return PyLong_FromLong(gpiod_chip_get_fd(self->chip));
+}
+
+static PyGetSetDef chip_getset[] = {
+       {
+               .name = "path",
+               .get = (getter)chip_path,
+       },
+       {
+               .name = "fd",
+               .get = (getter)chip_fd,
+       },
+       { }
+};
+
+static PyObject *chip_close(chip_object *self, PyObject *Py_UNUSED(ignored))
+{
+       Py_BEGIN_ALLOW_THREADS;
+       gpiod_chip_close(self->chip);
+       Py_END_ALLOW_THREADS;
+       self->chip = NULL;
+
+       Py_RETURN_NONE;
+}
+
+static PyObject *chip_get_info(chip_object *self, PyObject *Py_UNUSED(ignored))
+{
+       struct gpiod_chip_info *info;
+       PyObject *type, *ret;
+
+       type = Py_gpiod_GetGlobalType("ChipInfo");
+       if (!type)
+               return NULL;
+
+       info = gpiod_chip_get_info(self->chip);
+       if (!info)
+               return PyErr_SetFromErrno(PyExc_OSError);
+
+        ret = PyObject_CallFunction(type, "ssI",
+                                    gpiod_chip_info_get_name(info),
+                                    gpiod_chip_info_get_label(info),
+                                    gpiod_chip_info_get_num_lines(info));
+        gpiod_chip_info_free(info);
+        return ret;
+}
+
+static PyObject *make_line_info(struct gpiod_line_info *info)
+{
+       PyObject *type;
+
+       type = Py_gpiod_GetGlobalType("LineInfo");
+       if (!type)
+               return NULL;
+
+       return PyObject_CallFunction(type, "IsOsiOiiiiOk",
+                               gpiod_line_info_get_offset(info),
+                               gpiod_line_info_get_name(info),
+                               gpiod_line_info_is_used(info) ?
+                                                       Py_True : Py_False,
+                               gpiod_line_info_get_consumer(info),
+                               gpiod_line_info_get_direction(info),
+                               gpiod_line_info_is_active_low(info) ?
+                                                       Py_True : Py_False,
+                               gpiod_line_info_get_bias(info),
+                               gpiod_line_info_get_drive(info),
+                               gpiod_line_info_get_edge_detection(info),
+                               gpiod_line_info_get_event_clock(info),
+                               gpiod_line_info_is_debounced(info) ?
+                                                       Py_True : Py_False,
+                               gpiod_line_info_get_debounce_period_us(info));
+}
+
+static PyObject *chip_get_line_info(chip_object *self, PyObject *args)
+{
+       struct gpiod_line_info *info;
+       unsigned int offset;
+       PyObject *info_obj;
+       int ret, watch;
+
+       ret = PyArg_ParseTuple(args, "Ip", &offset, &watch);
+       if (!ret)
+               return NULL;
+
+       Py_BEGIN_ALLOW_THREADS;
+       if (watch)
+               info = gpiod_chip_watch_line_info(self->chip, offset);
+       else
+               info = gpiod_chip_get_line_info(self->chip, offset);
+       Py_END_ALLOW_THREADS;
+       if (!info)
+               return Py_gpiod_SetErrFromErrno();
+
+       info_obj = make_line_info(info);
+       gpiod_line_info_free(info);
+       return info_obj;
+}
+
+static PyObject *
+chip_unwatch_line_info(chip_object *self, PyObject *args)
+{
+       unsigned int offset;
+       int ret;
+
+       ret = PyArg_ParseTuple(args, "I", &offset);
+       if (!ret)
+               return NULL;
+
+       Py_BEGIN_ALLOW_THREADS;
+       ret = gpiod_chip_unwatch_line_info(self->chip, offset);
+       Py_END_ALLOW_THREADS;
+       if (ret)
+               return Py_gpiod_SetErrFromErrno();
+
+       Py_RETURN_NONE;
+}
+
+static PyObject *
+chip_read_info_event(chip_object *self, PyObject *Py_UNUSED(ignored))
+{
+       PyObject *type, *info_obj, *event_obj;
+       struct gpiod_info_event *event;
+       struct gpiod_line_info *info;
+
+       type = Py_gpiod_GetGlobalType("InfoEvent");
+       if (!type)
+               return NULL;
+
+       Py_BEGIN_ALLOW_THREADS;
+       event = gpiod_chip_read_info_event(self->chip);
+       Py_END_ALLOW_THREADS;
+       if (!event)
+               return Py_gpiod_SetErrFromErrno();
+
+       info = gpiod_info_event_get_line_info(event);
+
+       info_obj = make_line_info(info);
+       if (!info_obj) {
+               gpiod_info_event_free(event);
+               return NULL;
+       }
+
+       event_obj = PyObject_CallFunction(type, "iKO",
+                               gpiod_info_event_get_event_type(event),
+                               gpiod_info_event_get_timestamp_ns(event),
+                               info_obj);
+       Py_DECREF(info_obj);
+       gpiod_info_event_free(event);
+       return event_obj;
+}
+
+static PyObject *chip_line_offset_from_id(chip_object *self, PyObject *args)
+{
+       int ret, offset;
+       char *name;
+
+       ret = PyArg_ParseTuple(args, "s", &name);
+       if (!ret)
+               return NULL;
+
+       Py_BEGIN_ALLOW_THREADS;
+       offset = gpiod_chip_get_line_offset_from_name(self->chip, name);
+       Py_END_ALLOW_THREADS;
+       if (offset < 0)
+               return Py_gpiod_SetErrFromErrno();
+
+       return PyLong_FromLong(offset);
+}
+
+static struct gpiod_request_config *
+make_request_config(PyObject *consumer_obj, PyObject *event_buffer_size_obj)
+{
+       struct gpiod_request_config *req_cfg;
+       size_t event_buffer_size;
+       const char *consumer;
+
+       req_cfg = gpiod_request_config_new();
+       if (!req_cfg) {
+               Py_gpiod_SetErrFromErrno();
+               return NULL;
+       }
+
+       if (consumer_obj != Py_None) {
+               consumer = PyUnicode_AsUTF8(consumer_obj);
+               if (!consumer) {
+                       gpiod_request_config_free(req_cfg);
+                       return NULL;
+               }
+
+               gpiod_request_config_set_consumer(req_cfg, consumer);
+       }
+
+       if (event_buffer_size_obj != Py_None) {
+               event_buffer_size = PyLong_AsSize_t(event_buffer_size_obj);
+               if (PyErr_Occurred()) {
+                       gpiod_request_config_free(req_cfg);
+                       return NULL;
+               }
+
+               gpiod_request_config_set_event_buffer_size(req_cfg,
+                                                          event_buffer_size);
+       }
+
+       return req_cfg;
+}
+
+static PyObject *chip_request_lines(chip_object *self, PyObject *args)
+{
+       PyObject *line_config, *consumer, *event_buffer_size, *req_obj;
+       struct gpiod_request_config *req_cfg;
+       struct gpiod_line_config *line_cfg;
+       struct gpiod_line_request *request;
+       int ret;
+
+       ret = PyArg_ParseTuple(args, "OOO",
+                              &line_config, &consumer, &event_buffer_size);
+       if (!ret)
+               return NULL;
+
+       line_cfg = Py_gpiod_LineConfigGetData(line_config);
+       if (!line_cfg)
+               return NULL;
+
+       req_cfg = make_request_config(consumer, event_buffer_size);
+       if (!req_cfg)
+               return NULL;
+
+       Py_BEGIN_ALLOW_THREADS;
+       request = gpiod_chip_request_lines(self->chip, req_cfg, line_cfg);
+       Py_END_ALLOW_THREADS;
+       if (!request) {
+               gpiod_request_config_free(req_cfg);
+               return Py_gpiod_SetErrFromErrno();
+       }
+
+       req_obj = Py_gpiod_MakeRequestObject(request,
+                       gpiod_request_config_get_event_buffer_size(req_cfg));
+       if (!req_obj)
+               gpiod_line_request_release(request);
+       gpiod_request_config_free(req_cfg);
+
+       return req_obj;
+}
+
+static PyMethodDef chip_methods[] = {
+       {
+               .ml_name = "close",
+               .ml_meth = (PyCFunction)chip_close,
+               .ml_flags = METH_NOARGS,
+       },
+       {
+               .ml_name = "get_info",
+               .ml_meth = (PyCFunction)chip_get_info,
+               .ml_flags = METH_NOARGS,
+       },
+       {
+               .ml_name = "get_line_info",
+               .ml_meth = (PyCFunction)chip_get_line_info,
+               .ml_flags = METH_VARARGS,
+       },
+       {
+               .ml_name = "unwatch_line_info",
+               .ml_meth = (PyCFunction)chip_unwatch_line_info,
+               .ml_flags = METH_VARARGS,
+       },
+       {
+               .ml_name = "read_info_event",
+               .ml_meth = (PyCFunction)chip_read_info_event,
+               .ml_flags = METH_NOARGS,
+       },
+       {
+               .ml_name = "line_offset_from_id",
+               .ml_meth = (PyCFunction)chip_line_offset_from_id,
+               .ml_flags = METH_VARARGS,
+       },
+       {
+               .ml_name = "request_lines",
+               .ml_meth = (PyCFunction)chip_request_lines,
+               .ml_flags = METH_VARARGS,
+       },
+       { }
+};
+
+PyTypeObject chip_type = {
+       PyVarObject_HEAD_INIT(NULL, 0)
+       .tp_name = "gpiod._ext.Chip",
+       .tp_basicsize = sizeof(chip_object),
+       .tp_flags = Py_TPFLAGS_DEFAULT,
+       .tp_new = PyType_GenericNew,
+       .tp_init = (initproc)chip_init,
+       .tp_finalize = (destructor)chip_finalize,
+       .tp_dealloc = (destructor)Py_gpiod_dealloc,
+       .tp_getset = chip_getset,
+       .tp_methods = chip_methods,
+};
diff --git a/bindings/python/gpiod/ext/common.c b/bindings/python/gpiod/ext/common.c
new file mode 100644 (file)
index 0000000..07fca8c
--- /dev/null
@@ -0,0 +1,92 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// SPDX-FileCopyrightText: 2022 Bartosz Golaszewski <brgl@bgdev.pl>
+
+#include "internal.h"
+
+/* Generic dealloc callback for all gpiod objects. */
+void Py_gpiod_dealloc(PyObject *self)
+{
+       int ret;
+
+       ret = PyObject_CallFinalizerFromDealloc(self);
+       if (ret < 0)
+               return;
+
+       PyObject_Del(self);
+}
+
+PyObject *Py_gpiod_SetErrFromErrno(void)
+{
+       PyObject *exc;
+
+       if (errno == ENOMEM)
+               return PyErr_NoMemory();
+
+       switch (errno) {
+       case EINVAL:
+               exc = PyExc_ValueError;
+               break;
+       case EOPNOTSUPP:
+               exc = PyExc_NotImplementedError;
+               break;
+       case EPIPE:
+               exc = PyExc_BrokenPipeError;
+               break;
+       case ECHILD:
+               exc = PyExc_ChildProcessError;
+               break;
+       case EINTR:
+               exc = PyExc_InterruptedError;
+               break;
+       case EEXIST:
+               exc = PyExc_FileExistsError;
+               break;
+       case ENOENT:
+               exc = PyExc_FileNotFoundError;
+               break;
+       case EISDIR:
+               exc = PyExc_IsADirectoryError;
+               break;
+       case ENOTDIR:
+               exc = PyExc_NotADirectoryError;
+               break;
+       case EPERM:
+               exc = PyExc_PermissionError;
+               break;
+       case ETIMEDOUT:
+               exc = PyExc_TimeoutError;
+               break;
+       default:
+               exc = PyExc_OSError;
+               break;
+       }
+
+       return PyErr_SetFromErrno(exc);
+}
+
+PyObject *Py_gpiod_GetGlobalType(const char *type_name)
+{
+       PyObject *globals;
+
+       globals = PyEval_GetGlobals();
+       if (!globals)
+               return NULL;
+
+       return PyDict_GetItemString(globals, type_name);
+}
+
+unsigned int Py_gpiod_PyLongAsUnsignedInt(PyObject *pylong)
+{
+       unsigned long tmp;
+
+       tmp = PyLong_AsUnsignedLong(pylong);
+       if (PyErr_Occurred())
+               return 0;
+
+       if (tmp > UINT_MAX) {
+               PyErr_SetString(PyExc_ValueError, "value exceeding UINT_MAX");
+               return 0;
+       }
+
+       return tmp;
+}
diff --git a/bindings/python/gpiod/ext/internal.h b/bindings/python/gpiod/ext/internal.h
new file mode 100644 (file)
index 0000000..7d223c0
--- /dev/null
@@ -0,0 +1,19 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/* SPDX-FileCopyrightText: 2022 Bartosz Golaszewski <brgl@bgdev.pl> */
+
+#ifndef __LIBGPIOD_PYTHON_MODULE_H__
+#define __LIBGPIOD_PYTHON_MODULE_H__
+
+#include <gpiod.h>
+#include <Python.h>
+
+PyObject *Py_gpiod_SetErrFromErrno(void);
+PyObject *Py_gpiod_GetGlobalType(const char *type_name);
+unsigned int Py_gpiod_PyLongAsUnsignedInt(PyObject *pylong);
+void Py_gpiod_dealloc(PyObject *self);
+PyObject *Py_gpiod_MakeRequestObject(struct gpiod_line_request *request,
+                                    size_t event_buffer_size);
+struct gpiod_line_config *Py_gpiod_LineConfigGetData(PyObject *obj);
+struct gpiod_line_settings *Py_gpiod_LineSettingsGetData(PyObject *obj);
+
+#endif /* __LIBGPIOD_PYTHON_MODULE_H__ */
diff --git a/bindings/python/gpiod/ext/line-config.c b/bindings/python/gpiod/ext/line-config.c
new file mode 100644 (file)
index 0000000..0bba112
--- /dev/null
@@ -0,0 +1,197 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// SPDX-FileCopyrightText: 2022 Bartosz Golaszewski <brgl@bgdev.pl>
+
+#include "internal.h"
+
+typedef struct {
+       PyObject_HEAD;
+       struct gpiod_line_config *cfg;
+} line_config_object;
+
+static int line_config_init(line_config_object *self,
+                      PyObject *Py_UNUSED(args), PyObject *Py_UNUSED(ignored))
+{
+       self->cfg = gpiod_line_config_new();
+       if (!self->cfg) {
+               Py_gpiod_SetErrFromErrno();
+               return -1;
+       }
+
+       return 0;
+}
+
+static void line_config_finalize(line_config_object *self)
+{
+       if (self->cfg)
+               gpiod_line_config_free(self->cfg);
+}
+
+static unsigned int *make_offsets(PyObject *obj, Py_ssize_t len)
+{
+       unsigned int *offsets;
+       PyObject *offset;
+       Py_ssize_t i;
+
+       offsets = PyMem_Calloc(len, sizeof(unsigned int));
+       if (!offsets)
+               return (unsigned int *)PyErr_NoMemory();
+
+       for (i = 0; i < len; i++) {
+               offset = PyList_GetItem(obj, i);
+               if (!offset) {
+                       PyMem_Free(offsets);
+                       return NULL;
+               }
+
+               offsets[i] = Py_gpiod_PyLongAsUnsignedInt(offset);
+               if (PyErr_Occurred()) {
+                       PyMem_Free(offsets);
+                       return NULL;
+               }
+       }
+
+       return offsets;
+}
+
+static PyObject *
+line_config_add_line_settings(line_config_object *self, PyObject *args)
+{
+       PyObject *offsets_obj, *settings_obj;
+       struct gpiod_line_settings *settings;
+       unsigned int *offsets;
+       Py_ssize_t num_offsets;
+       int ret;
+
+       ret = PyArg_ParseTuple(args, "OO", &offsets_obj, &settings_obj);
+       if (!ret)
+               return NULL;
+
+       num_offsets = PyObject_Size(offsets_obj);
+       if (num_offsets < 0)
+               return NULL;
+
+       offsets = make_offsets(offsets_obj, num_offsets);
+       if (!offsets)
+               return NULL;
+
+       settings = Py_gpiod_LineSettingsGetData(settings_obj);
+       if (!settings) {
+               PyMem_Free(offsets);
+               return NULL;
+       }
+
+       ret = gpiod_line_config_add_line_settings(self->cfg, offsets,
+                                                 num_offsets, settings);
+       PyMem_Free(offsets);
+       if (ret)
+               return Py_gpiod_SetErrFromErrno();
+
+       Py_RETURN_NONE;
+}
+
+static PyObject *
+line_config_set_output_values(line_config_object *self, PyObject *args)
+{
+       PyObject *values, *iter, *next, *val_stripped;
+       enum gpiod_line_value *valbuf;
+       Py_ssize_t num_values, pos;
+       int ret;
+
+       values = PyTuple_GetItem(args, 0);
+       if (!values)
+               return NULL;
+
+       num_values = PyObject_Size(values);
+       if (num_values < 0)
+               return NULL;
+
+       valbuf = PyMem_Calloc(num_values, sizeof(*valbuf));
+       if (!valbuf)
+               return PyErr_NoMemory();
+
+       iter = PyObject_GetIter(values);
+       if (!iter) {
+               PyMem_Free(valbuf);
+               return NULL;
+       }
+
+       for (pos = 0;; pos++) {
+               next = PyIter_Next(iter);
+               if (!next) {
+                       Py_DECREF(iter);
+                       break;
+               }
+
+               val_stripped = PyObject_GetAttrString(next, "value");
+               Py_DECREF(next);
+               if (!val_stripped) {
+                       PyMem_Free(valbuf);
+                       Py_DECREF(iter);
+                       return NULL;
+               }
+
+               valbuf[pos] = PyLong_AsLong(val_stripped);
+               Py_DECREF(val_stripped);
+               if (PyErr_Occurred()) {
+                       PyMem_Free(valbuf);
+                       Py_DECREF(iter);
+                       return NULL;
+               }
+       }
+
+       ret = gpiod_line_config_set_output_values(self->cfg,
+                                                 valbuf, num_values);
+       PyMem_Free(valbuf);
+       if (ret)
+               return Py_gpiod_SetErrFromErrno();      
+
+       Py_RETURN_NONE;
+}
+
+static PyMethodDef line_config_methods[] = {
+       {
+               .ml_name = "add_line_settings",
+               .ml_meth = (PyCFunction)line_config_add_line_settings,
+               .ml_flags = METH_VARARGS,
+       },
+       {
+               .ml_name = "set_output_values",
+               .ml_meth = (PyCFunction)line_config_set_output_values,
+               .ml_flags = METH_VARARGS,
+       },
+       { }
+};
+
+PyTypeObject line_config_type = {
+       PyVarObject_HEAD_INIT(NULL, 0)
+       .tp_name = "gpiod._ext.LineConfig",
+       .tp_basicsize = sizeof(line_config_object),
+       .tp_flags = Py_TPFLAGS_DEFAULT,
+       .tp_new = PyType_GenericNew,
+       .tp_init = (initproc)line_config_init,
+       .tp_finalize = (destructor)line_config_finalize,
+       .tp_dealloc = (destructor)Py_gpiod_dealloc,
+       .tp_methods = line_config_methods,
+};
+
+struct gpiod_line_config *Py_gpiod_LineConfigGetData(PyObject *obj)
+{
+       line_config_object *line_cfg;
+       PyObject *type;
+
+       type = PyObject_Type(obj);
+       if (!type)
+               return NULL;
+
+       if ((PyTypeObject *)type != &line_config_type) {
+               PyErr_SetString(PyExc_TypeError,
+                               "not a gpiod._ext.LineConfig object");
+               Py_DECREF(type);
+               return NULL;
+       }
+       Py_DECREF(type);
+
+       line_cfg = (line_config_object *)obj;
+
+       return line_cfg->cfg;
+}
diff --git a/bindings/python/gpiod/ext/line-settings.c b/bindings/python/gpiod/ext/line-settings.c
new file mode 100644 (file)
index 0000000..650235e
--- /dev/null
@@ -0,0 +1,126 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// SPDX-FileCopyrightText: 2022 Bartosz Golaszewski <brgl@bgdev.pl>
+
+#include "internal.h"
+
+typedef struct {
+       PyObject_HEAD;
+       struct gpiod_line_settings *settings;
+} line_settings_object;
+
+static int set_error(void)
+{
+       Py_gpiod_SetErrFromErrno();
+       return -1;
+}
+
+static int
+line_settings_init(line_settings_object *self, PyObject *args, PyObject *kwargs)
+{
+       static char *kwlist[] = {
+               "direction",
+               "edge_detection",
+               "bias",
+               "drive",
+               "active_low",
+               "debounce_period",
+               "event_clock",
+               "output_value",
+               NULL
+       };
+
+       enum gpiod_line_clock event_clock;
+       enum gpiod_line_direction direction;
+       enum gpiod_line_value output_value;
+       unsigned long debounce_period;
+       enum gpiod_line_drive drive;
+       enum gpiod_line_edge edge;
+       enum gpiod_line_bias bias;
+       int ret, active_low;
+
+       ret = PyArg_ParseTupleAndKeywords(args, kwargs, "IIIIpkII", kwlist,
+                       &direction, &edge, &bias, &drive, &active_low,
+                       &debounce_period, &event_clock, &output_value);
+       if (!ret)
+               return -1;
+
+       self->settings = gpiod_line_settings_new();
+       if (!self->settings) {
+               Py_gpiod_SetErrFromErrno();
+               return -1;
+       }
+
+       ret = gpiod_line_settings_set_direction(self->settings, direction);
+       if (ret)
+               return set_error();
+
+       ret = gpiod_line_settings_set_edge_detection(self->settings, edge);
+       if (ret)
+               return set_error();
+
+       ret = gpiod_line_settings_set_bias(self->settings, bias);
+       if (ret)
+               return set_error();
+
+       ret = gpiod_line_settings_set_drive(self->settings, drive);
+       if (ret)
+               return set_error();
+
+       gpiod_line_settings_set_active_low(self->settings, active_low);
+       gpiod_line_settings_set_debounce_period_us(self->settings,
+                                                  debounce_period);
+
+       ret = gpiod_line_settings_set_edge_detection(self->settings, edge);
+       if (ret)
+               return set_error();
+
+       ret = gpiod_line_settings_set_output_value(self->settings,
+                                                  output_value);
+       if (ret)
+               return set_error();
+
+       ret = gpiod_line_settings_set_event_clock(self->settings, event_clock);
+       if (ret)
+               return set_error();
+
+       return 0;
+}
+
+static void line_settings_finalize(line_settings_object *self)
+{
+       if (self->settings)
+               gpiod_line_settings_free(self->settings);
+}
+
+PyTypeObject line_settings_type = {
+       PyVarObject_HEAD_INIT(NULL, 0)
+       .tp_name = "gpiod._ext.LineSettings",
+       .tp_basicsize = sizeof(line_settings_object),
+       .tp_flags = Py_TPFLAGS_DEFAULT,
+       .tp_new = PyType_GenericNew,
+       .tp_init = (initproc)line_settings_init,
+       .tp_finalize = (destructor)line_settings_finalize,
+       .tp_dealloc = (destructor)Py_gpiod_dealloc,
+};
+
+struct gpiod_line_settings *Py_gpiod_LineSettingsGetData(PyObject *obj)
+{
+       line_settings_object *settings;
+       PyObject *type;
+
+       type = PyObject_Type(obj);
+       if (!type)
+               return NULL;
+
+       if ((PyTypeObject *)type != &line_settings_type) {
+               PyErr_SetString(PyExc_TypeError,
+                               "not a gpiod._ext.LineSettings object");
+               Py_DECREF(type);
+               return NULL;
+       }
+       Py_DECREF(type);
+
+       settings = (line_settings_object *)obj;
+
+       return settings->settings;
+}
diff --git a/bindings/python/gpiod/ext/module.c b/bindings/python/gpiod/ext/module.c
new file mode 100644 (file)
index 0000000..b456190
--- /dev/null
@@ -0,0 +1,206 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// SPDX-FileCopyrightText: 2022 Bartosz Golaszewski <brgl@bgdev.pl>
+
+#include <gpiod.h>
+#include <Python.h>
+
+struct module_const {
+       const char *name;
+       long val;
+};
+
+static const struct module_const module_constants[] = {
+       {
+               .name = "VALUE_INACTIVE",
+               .val = GPIOD_LINE_VALUE_INACTIVE,
+       },
+       {
+               .name = "VALUE_ACTIVE",
+               .val = GPIOD_LINE_VALUE_ACTIVE,
+       },
+       {
+               .name = "DIRECTION_AS_IS",
+               .val = GPIOD_LINE_DIRECTION_AS_IS,
+       },
+       {
+               .name = "DIRECTION_INPUT",
+               .val = GPIOD_LINE_DIRECTION_INPUT,
+       },
+       {
+               .name = "DIRECTION_OUTPUT",
+               .val = GPIOD_LINE_DIRECTION_OUTPUT,
+       },
+       {
+               .name = "BIAS_AS_IS",
+               .val = GPIOD_LINE_BIAS_AS_IS,
+       },
+       {
+               .name = "BIAS_UNKNOWN",
+               .val = GPIOD_LINE_BIAS_UNKNOWN,
+       },
+       {
+               .name = "BIAS_DISABLED",
+               .val = GPIOD_LINE_BIAS_DISABLED,
+       },
+       {
+               .name = "BIAS_PULL_UP",
+               .val = GPIOD_LINE_BIAS_PULL_UP,
+       },
+       {
+               .name = "BIAS_PULL_DOWN",
+               .val = GPIOD_LINE_BIAS_PULL_DOWN,
+       },
+       {
+               .name = "DRIVE_PUSH_PULL",
+               .val = GPIOD_LINE_DRIVE_PUSH_PULL,
+       },
+       {
+               .name = "DRIVE_OPEN_DRAIN",
+               .val = GPIOD_LINE_DRIVE_OPEN_DRAIN,
+       },
+       {
+               .name = "DRIVE_OPEN_SOURCE",
+               .val = GPIOD_LINE_DRIVE_OPEN_SOURCE,
+       },
+       {
+               .name = "EDGE_NONE",
+               .val = GPIOD_LINE_EDGE_NONE,
+       },
+       {
+               .name = "EDGE_FALLING",
+               .val = GPIOD_LINE_EDGE_FALLING,
+       },
+       {
+               .name = "EDGE_RISING",
+               .val = GPIOD_LINE_EDGE_RISING,
+       },
+       {
+               .name = "EDGE_BOTH",
+               .val = GPIOD_LINE_EDGE_BOTH,
+       },
+       {
+               .name = "CLOCK_MONOTONIC",
+               .val = GPIOD_LINE_CLOCK_MONOTONIC,
+       },
+       {
+               .name = "CLOCK_REALTIME",
+               .val = GPIOD_LINE_CLOCK_REALTIME,
+       },
+       {
+               .name = "CLOCK_HTE",
+               .val = GPIOD_LINE_CLOCK_HTE,
+       },
+       {
+               .name = "EDGE_EVENT_TYPE_RISING",
+               .val = GPIOD_EDGE_EVENT_RISING_EDGE,
+       },
+       {
+               .name = "EDGE_EVENT_TYPE_FALLING",
+               .val = GPIOD_EDGE_EVENT_FALLING_EDGE,
+       },
+       {
+               .name = "INFO_EVENT_TYPE_LINE_REQUESTED",
+               .val = GPIOD_INFO_EVENT_LINE_REQUESTED,
+       },
+       {
+               .name = "INFO_EVENT_TYPE_LINE_RELEASED",
+               .val = GPIOD_INFO_EVENT_LINE_RELEASED,
+       },
+       {
+               .name = "INFO_EVENT_TYPE_LINE_CONFIG_CHANGED",
+               .val = GPIOD_INFO_EVENT_LINE_CONFIG_CHANGED,
+       },
+       { }
+};
+
+static PyObject *
+module_is_gpiochip_device(PyObject *Py_UNUSED(self), PyObject *args)
+{
+       const char *path;
+       int ret;
+
+       ret =  PyArg_ParseTuple(args, "s", &path);
+       if (!ret)
+               return NULL;
+
+       return PyBool_FromLong(gpiod_is_gpiochip_device(path));
+}
+
+static PyMethodDef module_methods[] = {
+       {
+               .ml_name = "is_gpiochip_device",
+               .ml_meth = (PyCFunction)module_is_gpiochip_device,
+               .ml_flags = METH_VARARGS,
+       },
+       { }
+};
+
+static PyModuleDef module_def = {
+       PyModuleDef_HEAD_INIT,
+       .m_name = "gpiod._ext",
+       .m_methods = module_methods,
+};
+
+extern PyTypeObject chip_type;
+extern PyTypeObject line_config_type;
+extern PyTypeObject line_settings_type;
+extern PyTypeObject request_type;
+
+static PyTypeObject *types[] = {
+       &chip_type,
+       &line_config_type,
+       &line_settings_type,
+       &request_type,
+       NULL,
+};
+
+PyMODINIT_FUNC PyInit__ext(void)
+{
+       const struct module_const *modconst;
+       PyObject *module, *all;
+       PyTypeObject **type;
+       int ret;
+
+       module = PyModule_Create(&module_def);
+       if (!module)
+               return NULL;
+
+       ret = PyModule_AddStringConstant(module, "api_version",
+                                        gpiod_api_version());
+       if (ret) {
+               Py_DECREF(module);
+               return NULL;
+       }
+
+       all = PyList_New(0);
+       if (!all) {
+               Py_DECREF(module);
+               return NULL;
+       }
+
+       ret = PyModule_AddObject(module, "__all__", all);
+       if (ret) {
+               Py_DECREF(all);
+               Py_DECREF(module);
+               return NULL;
+       }
+
+       for (type = types; *type; type++) {
+               ret = PyModule_AddType(module, *type);
+               if (ret) {
+                       Py_DECREF(module);
+                       return NULL;
+               }
+       }
+
+       for (modconst = module_constants; modconst->name; modconst++) {
+               ret = PyModule_AddIntConstant(module,
+                                             modconst->name, modconst->val);
+               if (ret) {
+                       Py_DECREF(module);
+                       return NULL;
+               }
+       }
+
+       return module;
+}
diff --git a/bindings/python/gpiod/ext/request.c b/bindings/python/gpiod/ext/request.c
new file mode 100644 (file)
index 0000000..5db69fe
--- /dev/null
@@ -0,0 +1,413 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// SPDX-FileCopyrightText: 2022 Bartosz Golaszewski <brgl@bgdev.pl>
+
+#include "internal.h"
+
+typedef struct {
+       PyObject_HEAD;
+       struct gpiod_line_request *request;
+       unsigned int *offsets;
+       enum gpiod_line_value *values;
+       size_t num_lines;
+       struct gpiod_edge_event_buffer *buffer;
+} request_object;
+
+static int request_init(PyObject *Py_UNUSED(ignored0),
+                       PyObject *Py_UNUSED(ignored1),
+                       PyObject *Py_UNUSED(ignored2))
+{
+       PyErr_SetString(PyExc_NotImplementedError,
+                       "_ext.LineRequest cannot be instantiated");
+
+       return -1;
+}
+
+static void request_finalize(request_object *self)
+{
+       if (self->request)
+               PyObject_CallMethod((PyObject *)self, "release", "");
+
+       if (self->offsets)
+               PyMem_Free(self->offsets);
+
+       if (self->values)
+               PyMem_Free(self->values);
+
+       if (self->buffer)
+               gpiod_edge_event_buffer_free(self->buffer);
+}
+
+static PyObject *
+request_chip_name(request_object *self, void *Py_UNUSED(ignored))
+{
+       return PyUnicode_FromString(
+                       gpiod_line_request_get_chip_name(self->request));
+}
+
+static PyObject *
+request_num_lines(request_object *self, void *Py_UNUSED(ignored))
+{
+       return PyLong_FromUnsignedLong(
+                       gpiod_line_request_get_num_requested_lines(self->request));
+}
+
+static PyObject *request_offsets(request_object *self, void *Py_UNUSED(ignored))
+{
+       PyObject *lines, *line;
+       unsigned int *offsets;
+       size_t num_lines, i;
+       int ret;
+
+       num_lines = gpiod_line_request_get_num_requested_lines(self->request);
+
+       offsets = PyMem_Calloc(num_lines, sizeof(unsigned int));
+       if (!offsets)
+               return PyErr_NoMemory();
+
+       gpiod_line_request_get_requested_offsets(self->request, offsets, num_lines);
+
+       lines = PyList_New(num_lines);
+       if (!lines) {
+               PyMem_Free(offsets);
+               return NULL;
+       }
+
+       for (i = 0; i < num_lines; i++) {
+               line = PyLong_FromUnsignedLong(offsets[i]);
+               if (!line) {
+                       Py_DECREF(lines);
+                       PyMem_Free(offsets);
+                       return NULL;
+               }
+
+               ret = PyList_SetItem(lines, i, line);
+               if (ret) {
+                       Py_DECREF(line);
+                       Py_DECREF(lines);
+                       PyMem_Free(offsets);
+                       return NULL;
+               }
+       }
+
+       PyMem_Free(offsets);
+       return lines;
+}
+
+static PyObject *request_fd(request_object *self, void *Py_UNUSED(ignored))
+{
+       return PyLong_FromLong(gpiod_line_request_get_fd(self->request));
+}
+
+static PyGetSetDef request_getset[] = {
+       {
+               .name = "chip_name",
+               .get = (getter)request_chip_name,
+       },
+       {
+               .name = "num_lines",
+               .get = (getter)request_num_lines,
+       },
+       {
+               .name = "offsets",
+               .get = (getter)request_offsets,
+       },
+       {
+               .name = "fd",
+               .get = (getter)request_fd,
+       },
+       { }
+};
+
+static PyObject *
+request_release(request_object *self, PyObject *Py_UNUSED(ignored))
+{
+       Py_BEGIN_ALLOW_THREADS;
+       gpiod_line_request_release(self->request);
+       Py_END_ALLOW_THREADS;
+       self->request = NULL;
+
+       Py_RETURN_NONE;
+}
+
+static void clear_buffers(request_object *self)
+{
+       memset(self->offsets, 0, self->num_lines * sizeof(unsigned int));
+       memset(self->values, 0, self->num_lines * sizeof(int));
+}
+
+static PyObject *request_get_values(request_object *self, PyObject *args)
+{
+       PyObject *offsets, *values, *val, *type, *iter, *next;
+       Py_ssize_t num_offsets, pos;
+       int ret;
+
+       ret = PyArg_ParseTuple(args, "OO", &offsets, &values);
+       if (!ret)
+               return NULL;
+
+       num_offsets = PyObject_Size(offsets);
+       if (num_offsets < 0)
+               return NULL;
+
+       type = Py_gpiod_GetGlobalType("Value");
+       if (!type)
+               return NULL;
+
+       iter = PyObject_GetIter(offsets);
+       if (!iter)
+               return NULL;
+
+       clear_buffers(self);
+
+       for (pos = 0;; pos++) {
+               next = PyIter_Next(iter);
+               if (!next) {
+                       Py_DECREF(iter);
+                       break;
+               }
+
+               self->offsets[pos] = Py_gpiod_PyLongAsUnsignedInt(next);
+               Py_DECREF(next);
+               if (PyErr_Occurred()) {
+                       Py_DECREF(iter);
+                       return NULL;
+               }
+       }
+
+       Py_BEGIN_ALLOW_THREADS;
+       ret = gpiod_line_request_get_values_subset(self->request,
+                                                  num_offsets,
+                                                  self->offsets,
+                                                  self->values);
+       Py_END_ALLOW_THREADS;
+       if (ret)
+               return Py_gpiod_SetErrFromErrno();
+
+       for (pos = 0; pos < num_offsets; pos++) {
+               val = PyObject_CallFunction(type, "i", self->values[pos]);
+               if (!val)
+                       return NULL;
+
+               ret = PyList_SetItem(values, pos, val);
+               if (ret) {
+                       Py_DECREF(val);
+                       return NULL;
+               }
+       }
+
+       Py_RETURN_NONE;
+}
+
+static PyObject *request_set_values(request_object *self, PyObject *args)
+{
+       PyObject *values, *key, *val, *val_stripped;
+       Py_ssize_t pos = 0;
+       int ret;
+
+       ret = PyArg_ParseTuple(args, "O", &values);
+       if (!ret)
+               return NULL;
+
+       clear_buffers(self);
+
+       while (PyDict_Next(values, &pos, &key, &val)) {
+               self->offsets[pos - 1] = Py_gpiod_PyLongAsUnsignedInt(key);
+               if (PyErr_Occurred())
+                       return NULL;
+
+               val_stripped = PyObject_GetAttrString(val, "value");
+               if (!val_stripped)
+                       return NULL;
+
+               self->values[pos - 1] = PyLong_AsLong(val_stripped);
+               Py_DECREF(val_stripped);
+               if (PyErr_Occurred())
+                       return NULL;
+       }
+
+       Py_BEGIN_ALLOW_THREADS;
+       ret = gpiod_line_request_set_values_subset(self->request,
+                                                  pos,
+                                                  self->offsets,
+                                                  self->values);
+       Py_END_ALLOW_THREADS;
+       if (ret)
+               return Py_gpiod_SetErrFromErrno();
+
+       Py_RETURN_NONE;
+}
+
+static PyObject *request_reconfigure_lines(request_object *self, PyObject *args)
+{
+       struct gpiod_line_config *line_cfg;
+       PyObject *line_cfg_obj;
+       int ret;
+
+       ret = PyArg_ParseTuple(args, "O", &line_cfg_obj);
+       if (!ret)
+               return NULL;
+
+       line_cfg = Py_gpiod_LineConfigGetData(line_cfg_obj);
+       if (!line_cfg)
+               return NULL;
+
+       Py_BEGIN_ALLOW_THREADS;
+       ret = gpiod_line_request_reconfigure_lines(self->request, line_cfg);
+       Py_END_ALLOW_THREADS;
+       if (ret)
+               return Py_gpiod_SetErrFromErrno();
+
+       Py_RETURN_NONE;
+}
+
+static PyObject *request_read_edge_events(request_object *self, PyObject *args)
+{
+       PyObject *max_events_obj, *event_obj, *events, *type;
+       size_t max_events, num_events, i;
+       struct gpiod_edge_event *event;
+       int ret;
+
+       ret = PyArg_ParseTuple(args, "O", &max_events_obj);
+       if (!ret)
+               return NULL;
+
+       if (max_events_obj != Py_None) {
+               max_events = PyLong_AsSize_t(max_events_obj);
+               if (PyErr_Occurred())
+                       return NULL;
+       } else {
+               max_events = 64;
+       }
+
+       type = Py_gpiod_GetGlobalType("EdgeEvent");
+       if (!type)
+               return NULL;
+
+       Py_BEGIN_ALLOW_THREADS;
+       ret = gpiod_line_request_read_edge_events(self->request,
+                                                self->buffer, max_events);
+       Py_END_ALLOW_THREADS;
+       if (ret < 0)
+               return Py_gpiod_SetErrFromErrno();
+
+       num_events = ret;
+
+       events = PyList_New(num_events);
+       if (!events)
+               return NULL;
+
+       for (i = 0; i < num_events; i++) {
+               event = gpiod_edge_event_buffer_get_event(self->buffer, i);
+               if (!event) {
+                       Py_DECREF(events);
+                       return NULL;
+               }
+
+               event_obj = PyObject_CallFunction(type, "iKiii",
+                               gpiod_edge_event_get_event_type(event),
+                               gpiod_edge_event_get_timestamp_ns(event),
+                               gpiod_edge_event_get_line_offset(event),
+                               gpiod_edge_event_get_global_seqno(event),
+                               gpiod_edge_event_get_line_seqno(event));
+               if (!event_obj) {
+                       Py_DECREF(events);
+                       return NULL;
+               }
+
+               ret = PyList_SetItem(events, i, event_obj);
+               if (ret) {
+                       Py_DECREF(event_obj);
+                       Py_DECREF(events);
+                       return NULL;
+               }
+       }
+
+       return events;
+}
+
+static PyMethodDef request_methods[] = {
+       {
+               .ml_name = "release",
+               .ml_meth = (PyCFunction)request_release,
+               .ml_flags = METH_NOARGS,
+       },
+       {
+               .ml_name = "get_values",
+               .ml_meth = (PyCFunction)request_get_values,
+               .ml_flags = METH_VARARGS,
+       },
+       {
+               .ml_name = "set_values",
+               .ml_meth = (PyCFunction)request_set_values,
+               .ml_flags = METH_VARARGS,
+       },
+       {
+               .ml_name = "reconfigure_lines",
+               .ml_meth = (PyCFunction)request_reconfigure_lines,
+               .ml_flags = METH_VARARGS,
+       },
+       {
+               .ml_name = "read_edge_events",
+               .ml_meth = (PyCFunction)request_read_edge_events,
+               .ml_flags = METH_VARARGS,
+       },
+       { }
+};
+
+PyTypeObject request_type = {
+       PyVarObject_HEAD_INIT(NULL, 0)
+       .tp_name = "gpiod._ext.Request",
+       .tp_basicsize = sizeof(request_object),
+       .tp_flags = Py_TPFLAGS_DEFAULT,
+       .tp_new = PyType_GenericNew,
+       .tp_init = (initproc)request_init,
+       .tp_finalize = (destructor)request_finalize,
+       .tp_dealloc = (destructor)Py_gpiod_dealloc,
+       .tp_getset = request_getset,
+       .tp_methods = request_methods,
+};
+
+PyObject *Py_gpiod_MakeRequestObject(struct gpiod_line_request *request,
+                                    size_t event_buffer_size)
+{
+       struct gpiod_edge_event_buffer *buffer;
+       enum gpiod_line_value *values;
+       request_object *req_obj;
+       unsigned int *offsets;
+       size_t num_lines;
+
+       num_lines = gpiod_line_request_get_num_requested_lines(request);
+
+       req_obj = PyObject_New(request_object, &request_type);
+       if (!req_obj)
+               return NULL;
+
+       offsets = PyMem_Calloc(num_lines, sizeof(unsigned int));
+       if (!offsets) {
+               Py_DECREF(req_obj);
+               return NULL;
+       }
+
+       values = PyMem_Calloc(num_lines, sizeof(int));
+       if (!values) {
+               PyMem_Free(offsets);
+               Py_DECREF(req_obj);
+               return NULL;
+       }
+
+       buffer = gpiod_edge_event_buffer_new(event_buffer_size);
+       if (!buffer) {
+               PyMem_Free(values);
+               PyMem_Free(offsets);
+               Py_DECREF(req_obj);
+               return Py_gpiod_SetErrFromErrno();
+       }
+
+       req_obj->request = request;
+       req_obj->offsets = offsets;
+       req_obj->values = values;
+       req_obj->num_lines = num_lines;
+       req_obj->buffer = buffer;
+
+       return (PyObject *)req_obj;
+}
diff --git a/bindings/python/gpiod/info_event.py b/bindings/python/gpiod/info_event.py
new file mode 100644 (file)
index 0000000..481eae6
--- /dev/null
@@ -0,0 +1,35 @@
+# SPDX-License-Identifier: LGPL-2.1-or-later
+# SPDX-FileCopyrightText: 2022 Bartosz Golaszewski <brgl@bgdev.pl>
+
+from . import _ext
+from .line_info import LineInfo
+from dataclasses import dataclass
+from enum import Enum
+
+__all__ = "InfoEvent"
+
+
+@dataclass(frozen=True, init=False, repr=False)
+class InfoEvent:
+    """
+    Immutable object containing data about a single line info event.
+    """
+
+    class Type(Enum):
+        LINE_REQUESTED = _ext.INFO_EVENT_TYPE_LINE_REQUESTED
+        LINE_RELEASED = _ext.INFO_EVENT_TYPE_LINE_RELEASED
+        LINE_CONFIG_CHANGED = _ext.INFO_EVENT_TYPE_LINE_CONFIG_CHANGED
+
+    event_type: Type
+    timestamp_ns: int
+    line_info: LineInfo
+
+    def __init__(self, event_type: int, timestamp_ns: int, line_info: LineInfo):
+        object.__setattr__(self, "event_type", InfoEvent.Type(event_type))
+        object.__setattr__(self, "timestamp_ns", timestamp_ns)
+        object.__setattr__(self, "line_info", line_info)
+
+    def __str__(self):
+        return "<InfoEvent type={} timestamp_ns={} line_info={}>".format(
+            self.event_type, self.timestamp_ns, self.line_info
+        )
diff --git a/bindings/python/gpiod/internal.py b/bindings/python/gpiod/internal.py
new file mode 100644 (file)
index 0000000..2dddb65
--- /dev/null
@@ -0,0 +1,18 @@
+# SPDX-License-Identifier: GPL-2.0-or-later
+# SPDX-FileCopyrightText: 2022 Bartosz Golaszewski <brgl@bgdev.pl>
+
+from datetime import timedelta
+from select import select
+from typing import Optional, Union
+
+__all__ = []
+
+
+def poll_fd(fd: int, timeout: Optional[Union[timedelta, float]] = None) -> bool:
+    if isinstance(timeout, timedelta):
+        sec = timeout.total_seconds()
+    else:
+        sec = timeout
+
+    readable, _, _ = select([fd], [], [], sec)
+    return True if fd in readable else False
diff --git a/bindings/python/gpiod/line.py b/bindings/python/gpiod/line.py
new file mode 100644 (file)
index 0000000..1cc512f
--- /dev/null
@@ -0,0 +1,58 @@
+# SPDX-License-Identifier: LGPL-2.1-or-later
+# SPDX-FileCopyrightText: 2022 Bartosz Golaszewski <brgl@bgdev.pl>
+
+
+from . import _ext
+from enum import Enum
+
+__all__ = ["Value", "Direction", "Bias", "Drive", "Edge", "Clock"]
+
+
+class Value(Enum):
+    """Logical line states."""
+
+    INACTIVE = _ext.VALUE_INACTIVE
+    ACTIVE = _ext.VALUE_ACTIVE
+
+
+class Direction(Enum):
+    """Direction settings."""
+
+    AS_IS = _ext.DIRECTION_AS_IS
+    INPUT = _ext.DIRECTION_INPUT
+    OUTPUT = _ext.DIRECTION_OUTPUT
+
+
+class Bias(Enum):
+    """Internal bias settings."""
+
+    AS_IS = _ext.BIAS_AS_IS
+    UNKNOWN = _ext.BIAS_UNKNOWN
+    DISABLED = _ext.BIAS_DISABLED
+    PULL_UP = _ext.BIAS_PULL_UP
+    PULL_DOWN = _ext.BIAS_PULL_DOWN
+
+
+class Drive(Enum):
+    """Drive settings."""
+
+    PUSH_PULL = _ext.DRIVE_PUSH_PULL
+    OPEN_DRAIN = _ext.DRIVE_OPEN_DRAIN
+    OPEN_SOURCE = _ext.DRIVE_OPEN_SOURCE
+
+
+class Edge(Enum):
+    """Edge detection settings."""
+
+    NONE = _ext.EDGE_NONE
+    RISING = _ext.EDGE_RISING
+    FALLING = _ext.EDGE_FALLING
+    BOTH = _ext.EDGE_BOTH
+
+
+class Clock(Enum):
+    """Event clock settings."""
+
+    MONOTONIC = _ext.CLOCK_MONOTONIC
+    REALTIME = _ext.CLOCK_REALTIME
+    HTE = _ext.CLOCK_HTE
diff --git a/bindings/python/gpiod/line_info.py b/bindings/python/gpiod/line_info.py
new file mode 100644 (file)
index 0000000..c196a6a
--- /dev/null
@@ -0,0 +1,75 @@
+# SPDX-License-Identifier: LGPL-2.1-or-later
+# SPDX-FileCopyrightText: 2022 Bartosz Golaszewski <brgl@bgdev.pl>
+
+from . import _ext
+from dataclasses import dataclass
+from datetime import timedelta
+from gpiod.line import Direction, Bias, Drive, Edge, Clock
+
+__all__ = "LineInfo"
+
+
+@dataclass(frozen=True, init=False, repr=False)
+class LineInfo:
+    """
+    Snapshot of a line's status.
+    """
+
+    offset: int
+    name: str
+    used: bool
+    consumer: str
+    direction: Direction
+    active_low: bool
+    bias: Bias
+    drive: Drive
+    edge_detection: Edge
+    event_clock: Clock
+    debounced: bool
+    debounce_period: timedelta
+
+    def __init__(
+        self,
+        offset: int,
+        name: str,
+        used: bool,
+        consumer: str,
+        direction: int,
+        active_low: bool,
+        bias: int,
+        drive: int,
+        edge_detection: int,
+        event_clock: int,
+        debounced: bool,
+        debounce_period_us: int,
+    ):
+        object.__setattr__(self, "offset", offset)
+        object.__setattr__(self, "name", name)
+        object.__setattr__(self, "used", used)
+        object.__setattr__(self, "consumer", consumer)
+        object.__setattr__(self, "direction", Direction(direction))
+        object.__setattr__(self, "active_low", active_low)
+        object.__setattr__(self, "bias", Bias(bias))
+        object.__setattr__(self, "drive", Drive(drive))
+        object.__setattr__(self, "edge_detection", Edge(edge_detection))
+        object.__setattr__(self, "event_clock", Clock(event_clock))
+        object.__setattr__(self, "debounced", debounced)
+        object.__setattr__(
+            self, "debounce_period", timedelta(microseconds=debounce_period_us)
+        )
+
+    def __str__(self):
+        return '<LineInfo offset={} name="{}" used={} consumer="{}" direction={} active_low={} bias={} drive={} edge_detection={} event_clock={} debounced={} debounce_period={}>'.format(
+            self.offset,
+            self.name,
+            self.used,
+            self.consumer,
+            self.direction,
+            self.active_low,
+            self.bias,
+            self.drive,
+            self.edge_detection,
+            self.event_clock,
+            self.debounced,
+            self.debounce_period,
+        )
diff --git a/bindings/python/gpiod/line_request.py b/bindings/python/gpiod/line_request.py
new file mode 100644 (file)
index 0000000..cde298f
--- /dev/null
@@ -0,0 +1,258 @@
+# SPDX-License-Identifier: LGPL-2.1-or-later
+# SPDX-FileCopyrightText: 2022 Bartosz Golaszewski <brgl@bgdev.pl>
+
+from . import _ext
+from .edge_event import EdgeEvent
+from .exception import RequestReleasedError
+from .internal import poll_fd
+from .line import Value
+from .line_settings import LineSettings, _line_settings_to_ext
+from collections.abc import Iterable
+from datetime import timedelta
+from typing import Optional, Union
+
+__all__ = "LineRequest"
+
+
+class LineRequest:
+    """
+    Stores the context of a set of requested GPIO lines.
+    """
+
+    def __init__(self, req: _ext.Request):
+        """
+        DON'T USE
+
+        LineRequest objects can only be instantiated by a Chip parent. This is
+        not part of stable API.
+        """
+        self._req = req
+
+    def __bool__(self) -> bool:
+        """
+        Boolean conversion for GPIO line requests.
+
+        Returns:
+          True if the request is live and False if it's been released.
+        """
+        return True if self._req else False
+
+    def __enter__(self):
+        """
+        Controlled execution enter callback.
+        """
+        self._check_released()
+        return self
+
+    def __exit__(self, exc_type, exc_value, traceback):
+        """
+        Controlled execution exit callback.
+        """
+        self.release()
+
+    def _check_released(self) -> None:
+        if not self._req:
+            raise RequestReleasedError()
+
+    def release(self) -> None:
+        """
+        Release this request and free all associated resources. The object must
+        not be used after a call to this method.
+        """
+        self._check_released()
+        self._req.release()
+        self._req = None
+
+    def get_value(self, line: Union[int, str]) -> Value:
+        """
+        Get a single GPIO line value.
+
+        Args:
+          line:
+            Offset or name of the line to get value for.
+
+        Returns:
+          Logical value of the line.
+        """
+        return self.get_values([line])[0]
+
+    def _check_line_name(self, line):
+        if isinstance(line, str):
+            if line not in self._name_map:
+                raise ValueError("unknown line name: {}".format(line))
+
+            return True
+
+        return False
+
+    def get_values(
+        self, lines: Optional[Iterable[Union[int, str]]] = None
+    ) -> list[Value]:
+        """
+        Get values of a set of GPIO lines.
+
+        Args:
+          lines:
+            List of names or offsets of GPIO lines to get values for. Can be
+            None in which case all requested lines will be read.
+
+        Returns:
+          List of logical line values.
+        """
+        self._check_released()
+
+        lines = lines or self._lines
+
+        offsets = [
+            self._name_map[line] if self._check_line_name(line) else line
+            for line in lines
+        ]
+
+        buf = [None] * len(lines)
+
+        self._req.get_values(offsets, buf)
+        return buf
+
+    def set_value(self, line: Union[int, str], value: Value) -> None:
+        """
+        Set the value of a single GPIO line.
+
+        Args:
+          line:
+            Offset or name of the line to set.
+          value:
+            New value.
+        """
+        self.set_values({line: value})
+
+    def set_values(self, values: dict[Union[int, str], Value]) -> None:
+        """
+        Set the values of a subset of GPIO lines.
+
+        Args:
+          values:
+            Dictionary mapping line offsets or names to desired values.
+        """
+        self._check_released()
+
+        mapped = {
+            self._name_map[line] if self._check_line_name(line) else line: values[line]
+            for line in values
+        }
+
+        self._req.set_values(mapped)
+
+    def reconfigure_lines(
+        self, config: dict[tuple[Union[int, str]], LineSettings]
+    ) -> None:
+        """
+        Reconfigure requested lines.
+
+        Args:
+          config
+            Dictionary mapping offsets or names (or tuples thereof) to
+            LineSettings. If None is passed as the value of the mapping,
+            default settings are used.
+        """
+        self._check_released()
+
+        line_cfg = _ext.LineConfig()
+
+        for lines, settings in config.items():
+            if isinstance(lines, int) or isinstance(lines, str):
+                lines = [lines]
+
+            offsets = [
+                self._name_map[line] if self._check_line_name(line) else line
+                for line in lines
+            ]
+
+            line_cfg.add_line_settings(offsets, _line_settings_to_ext(settings))
+
+        self._req.reconfigure_lines(line_cfg)
+
+    def wait_edge_events(
+        self, timeout: Optional[Union[timedelta, float]] = None
+    ) -> bool:
+        """
+        Wait for edge events on any of the requested lines.
+
+        Args:
+          timeout:
+            Wait time limit expressed as either a datetime.timedelta object
+            or the number of seconds stored in a float. If set to 0, the
+            method returns immediately, if set to None it blocks indefinitely.
+
+        Returns:
+          True if events are ready to be read. False on timeout.
+        """
+        self._check_released()
+
+        return poll_fd(self.fd, timeout)
+
+    def read_edge_events(self, max_events: Optional[int] = None) -> list[EdgeEvent]:
+        """
+        Read a number of edge events from a line request.
+
+        Args:
+          max_events:
+            Maximum number of events to read.
+
+        Returns:
+          List of read EdgeEvent objects.
+        """
+        self._check_released()
+
+        return self._req.read_edge_events(max_events)
+
+    def __str__(self):
+        """
+        Return a user-friendly, human-readable description of this request.
+        """
+        if not self._req:
+            return "<LineRequest RELEASED>"
+
+        return '<LineRequest chip="{}" num_lines={} offsets={} fd={}>'.format(
+            self.chip_name, self.num_lines, self.offsets, self.fd
+        )
+
+    @property
+    def chip_name(self) -> str:
+        """
+        Name of the chip this request was made on.
+        """
+        self._check_released()
+        return self._chip_name
+
+    @property
+    def num_lines(self) -> int:
+        """
+        Number of requested lines.
+        """
+        self._check_released()
+        return len(self._offsets)
+
+    @property
+    def offsets(self) -> list[int]:
+        """
+        List of requested offsets. Lines requested by name are mapped to their
+        offsets.
+        """
+        self._check_released()
+        return self._offsets
+
+    @property
+    def lines(self) -> list[Union[int, str]]:
+        """
+        List of requested lines. Lines requested by name are listed as such.
+        """
+        self._check_released()
+        return self._lines
+
+    @property
+    def fd(self) -> int:
+        """
+        File descriptor associated with this request.
+        """
+        self._check_released()
+        return self._req.fd
diff --git a/bindings/python/gpiod/line_settings.py b/bindings/python/gpiod/line_settings.py
new file mode 100644 (file)
index 0000000..458fd81
--- /dev/null
@@ -0,0 +1,64 @@
+# SPDX-License-Identifier: LGPL-2.1-or-later
+# SPDX-FileCopyrightText: 2022 Bartosz Golaszewski <brgl@bgdev.pl>
+
+from . import _ext
+from dataclasses import dataclass
+from datetime import timedelta
+from gpiod.line import Direction, Bias, Drive, Edge, Clock, Value
+
+__all__ = "LineSettings"
+
+
+@dataclass(repr=False)
+class LineSettings:
+    """
+    Stores a set of line properties.
+    """
+
+    direction: Direction = Direction.AS_IS
+    edge_detection: Edge = Edge.NONE
+    bias: Bias = Bias.AS_IS
+    drive: Drive = Drive.PUSH_PULL
+    active_low: bool = False
+    debounce_period: timedelta = timedelta()
+    event_clock: Clock = Clock.MONOTONIC
+    output_value: Value = Value.INACTIVE
+
+    # __repr__ generated by @dataclass uses repr for enum members resulting in
+    # an unusable representation as those are of the form: <NAME: $value>
+    def __repr__(self):
+        return "LineSettings(direction={}, edge_detection={} bias={} drive={} active_low={} debounce_period={} event_clock={} output_value={})".format(
+            str(self.direction),
+            str(self.edge_detection),
+            str(self.bias),
+            str(self.drive),
+            self.active_low,
+            repr(self.debounce_period),
+            str(self.event_clock),
+            str(self.output_value),
+        )
+
+    def __str__(self):
+        return "<LineSettings direction={} edge_detection={} bias={} drive={} active_low={} debounce_period={} event_clock={} output_value={}>".format(
+            self.direction,
+            self.edge_detection,
+            self.bias,
+            self.drive,
+            self.active_low,
+            self.debounce_period,
+            self.event_clock,
+            self.output_value,
+        )
+
+
+def _line_settings_to_ext(settings: LineSettings) -> _ext.LineSettings:
+    return _ext.LineSettings(
+        direction=settings.direction.value,
+        edge_detection=settings.edge_detection.value,
+        bias=settings.bias.value,
+        drive=settings.drive.value,
+        active_low=settings.active_low,
+        debounce_period=int(settings.debounce_period.total_seconds() * 1000000),
+        event_clock=settings.event_clock.value,
+        output_value=settings.output_value.value,
+    )
diff --git a/bindings/python/gpiod/version.py b/bindings/python/gpiod/version.py
new file mode 100644 (file)
index 0000000..2e182a3
--- /dev/null
@@ -0,0 +1,5 @@
+# SPDX-License-Identifier: LGPL-2.1-or-later
+# SPDX-FileCopyrightText: 2022 Linaro Ltd.
+# SPDX-FileCopyrightText: 2022 Bartosz Golaszewski <bartosz.golaszewski@linaro.org>
+
+__version__ = "2.0.1"
diff --git a/bindings/python/pyproject.toml b/bindings/python/pyproject.toml
new file mode 100644 (file)
index 0000000..f6bf43c
--- /dev/null
@@ -0,0 +1,5 @@
+# SPDX-License-Identifier: GPL-2.0-or-later
+# SPDX-FileCopyrightText: 2023 Phil Howard <phil@gadgetoid.com>
+
+[build-system]
+requires = ["setuptools", "wheel", "packaging"]
diff --git a/bindings/python/setup.py b/bindings/python/setup.py
new file mode 100644 (file)
index 0000000..904b5f3
--- /dev/null
@@ -0,0 +1,264 @@
+# SPDX-License-Identifier: GPL-2.0-or-later
+# SPDX-FileCopyrightText: 2022 Bartosz Golaszewski <brgl@bgdev.pl>
+
+from os import getenv, path, unlink
+from shutil import copytree, rmtree
+
+from setuptools import Extension, find_packages, setup
+from setuptools.command.build_ext import build_ext as orig_build_ext
+from setuptools.command.sdist import log
+from setuptools.command.sdist import sdist as orig_sdist
+from setuptools.errors import BaseError
+
+LINK_SYSTEM_LIBGPIOD = getenv("LINK_SYSTEM_LIBGPIOD") == "1"
+LIBGPIOD_MINIMUM_VERSION = "2.1.0"
+LIBGPIOD_VERSION = getenv("LIBGPIOD_VERSION")
+GPIOD_WITH_TESTS = getenv("GPIOD_WITH_TESTS") == "1"
+SRC_BASE_URL = "https://mirrors.edge.kernel.org/pub/software/libs/libgpiod/"
+TAR_FILENAME = "libgpiod-{version}.tar.gz"
+ASC_FILENAME = "sha256sums.asc"
+SHA256_CHUNK_SIZE = 2048
+
+# __version__
+with open("gpiod/version.py", "r") as fd:
+    exec(fd.read())
+
+
+def sha256(filename):
+    """
+    Return a sha256sum for a specific filename, loading the file in chunks
+    to avoid potentially excessive memory use.
+    """
+    from hashlib import sha256
+
+    sha256sum = sha256()
+    with open(filename, "rb") as f:
+        for chunk in iter(lambda: f.read(SHA256_CHUNK_SIZE), b""):
+            sha256sum.update(chunk)
+
+    return sha256sum.hexdigest()
+
+
+def find_sha256sum(asc_file, tar_filename):
+    """
+    Search through a local copy of sha256sums.asc for a specific filename
+    and return the associated sha256 sum.
+    """
+    with open(asc_file, "r") as f:
+        for line in f:
+            line = line.strip().split("  ")
+            if len(line) == 2 and line[1] == tar_filename:
+                return line[0]
+
+    raise BaseError(f"no signature found for {tar_filename}")
+
+
+def fetch_tarball(command):
+    """
+    Verify the requested LIBGPIOD_VERSION tarball exists in sha256sums.asc,
+    fetch it from https://mirrors.edge.kernel.org/pub/software/libs/libgpiod/
+    and verify its sha256sum.
+
+    If the check passes, extract the tarball and copy the lib and include
+    dirs into our source tree.
+    """
+
+    # If no LIBGPIOD_VERSION is specified in env, just run the command
+    if LIBGPIOD_VERSION is None:
+        return command
+
+    # If LIBGPIOD_VERSION is specified, apply the tarball wrapper
+    def wrapper(self):
+        # Just-in-time import of tarfile and urllib.request so these are
+        # not required for Yocto to build a vendored or linked package
+        import tarfile
+        from tempfile import TemporaryDirectory
+        from urllib.request import urlretrieve
+
+        from packaging.version import Version
+
+        # The "build" frontend will run setup.py twice within the same
+        # temporary output directory. First for "sdist" and then for "wheel"
+        # This would cause the build to fail with dirty "lib" and "include"
+        # directories.
+        # If the version in "libgpiod-version.txt" already matches our
+        # requested tarball, then skip the fetch altogether.
+        try:
+            if open("libgpiod-version.txt", "r").read() == LIBGPIOD_VERSION:
+                log.info(f"skipping tarball fetch")
+                command(self)
+                return
+        except OSError:
+            pass
+
+        # Early exit for build tree with dirty lib/include dirs
+        for check_dir in "lib", "include":
+            if path.isdir(f"./{check_dir}"):
+                raise BaseError(f"refusing to overwrite ./{check_dir}")
+
+        with TemporaryDirectory(prefix="libgpiod-") as temp_dir:
+            tarball_filename = TAR_FILENAME.format(version=LIBGPIOD_VERSION)
+            tarball_url = f"{SRC_BASE_URL}{tarball_filename}"
+            asc_url = f"{SRC_BASE_URL}{ASC_FILENAME}"
+
+            log.info(f"fetching: {asc_url}")
+
+            asc_filename, _ = urlretrieve(asc_url, path.join(temp_dir, ASC_FILENAME))
+
+            tarball_sha256 = find_sha256sum(asc_filename, tarball_filename)
+
+            if Version(LIBGPIOD_VERSION) < Version(LIBGPIOD_MINIMUM_VERSION):
+                raise BaseError(f"requires libgpiod>={LIBGPIOD_MINIMUM_VERSION}")
+
+            log.info(f"fetching: {tarball_url}")
+
+            downloaded_tarball, _ = urlretrieve(
+                tarball_url, path.join(temp_dir, tarball_filename)
+            )
+
+            log.info(f"verifying: {tarball_filename}")
+            if sha256(downloaded_tarball) != tarball_sha256:
+                raise BaseError(f"signature mismatch for {tarball_filename}")
+
+            # Unpack the downloaded tarball
+            log.info(f"unpacking: {tarball_filename}")
+            with tarfile.open(downloaded_tarball) as f:
+                f.extractall(temp_dir)
+
+            # Copy the include and lib directories we need to build libgpiod
+            base_dir = path.join(temp_dir, f"libgpiod-{LIBGPIOD_VERSION}")
+            copytree(path.join(base_dir, "include"), "./include")
+            copytree(path.join(base_dir, "lib"), "./lib")
+
+        # Save the libgpiod version for sdist
+        open("libgpiod-version.txt", "w").write(LIBGPIOD_VERSION)
+
+        # Run the command
+        command(self)
+
+        # Clean up the build directory
+        rmtree("./lib", ignore_errors=True)
+        rmtree("./include", ignore_errors=True)
+        unlink("libgpiod-version.txt")
+
+    return wrapper
+
+
+class build_ext(orig_build_ext):
+    """
+    Wrap build_ext to amend the module sources and settings to build
+    the bindings and gpiod into a combined module when a version is
+    specified and LINK_SYSTEM_LIBGPIOD=1 is not present in env.
+
+    run is wrapped with @fetch_tarball in order to fetch the sources
+    needed to build binary wheels when LIBGPIOD_VERSION is specified, eg:
+
+    LIBGPIOD_VERSION="2.0.2" python3 -m build .
+    """
+
+    @fetch_tarball
+    def run(self):
+        # Try to get the gpiod version from the .txt file included in sdist
+        try:
+            libgpiod_version = open("libgpiod-version.txt", "r").read()
+        except OSError:
+            libgpiod_version = LIBGPIOD_VERSION
+
+        if libgpiod_version and not LINK_SYSTEM_LIBGPIOD:
+            # When building the extension from an sdist with a vendored
+            # amend gpiod._ext sources and settings accordingly.
+            gpiod_ext = self.ext_map["gpiod._ext"]
+            gpiod_ext.sources += [
+                "lib/chip.c",
+                "lib/chip-info.c",
+                "lib/edge-event.c",
+                "lib/info-event.c",
+                "lib/internal.c",
+                "lib/line-config.c",
+                "lib/line-info.c",
+                "lib/line-request.c",
+                "lib/line-settings.c",
+                "lib/misc.c",
+                "lib/request-config.c",
+            ]
+            gpiod_ext.libraries = []
+            gpiod_ext.include_dirs = ["include", "lib", "gpiod/ext"]
+            gpiod_ext.extra_compile_args.append(
+                f'-DGPIOD_VERSION_STR="{libgpiod_version}"',
+            )
+
+        super().run()
+
+        # We don't ever want the module tests directory in our package
+        # since this might include gpiosim._ext or procname._ext from a
+        # previous dirty build tree.
+        rmtree(path.join(self.build_lib, "tests"), ignore_errors=True)
+
+
+class sdist(orig_sdist):
+    """
+    Wrap sdist in order to fetch the libgpiod source files for vendoring
+    into a source distribution.
+
+    run is wrapped with @fetch_tarball in order to fetch the sources
+    needed to build binary wheels when LIBGPIOD_VERSION is specified, eg:
+
+    LIBGPIOD_VERSION="2.0.2" python3 -m build . --sdist
+    """
+
+    @fetch_tarball
+    def run(self):
+        super().run()
+
+
+gpiod_ext = Extension(
+    "gpiod._ext",
+    sources=[
+        "gpiod/ext/chip.c",
+        "gpiod/ext/common.c",
+        "gpiod/ext/line-config.c",
+        "gpiod/ext/line-settings.c",
+        "gpiod/ext/module.c",
+        "gpiod/ext/request.c",
+    ],
+    define_macros=[("_GNU_SOURCE", "1")],
+    libraries=["gpiod"],
+    extra_compile_args=["-Wall", "-Wextra"],
+)
+
+gpiosim_ext = Extension(
+    "tests.gpiosim._ext",
+    sources=["tests/gpiosim/ext.c"],
+    define_macros=[("_GNU_SOURCE", "1")],
+    libraries=["gpiosim"],
+    extra_compile_args=["-Wall", "-Wextra"],
+)
+
+procname_ext = Extension(
+    "tests.procname._ext",
+    sources=["tests/procname/ext.c"],
+    define_macros=[("_GNU_SOURCE", "1")],
+    extra_compile_args=["-Wall", "-Wextra"],
+)
+
+extensions = [gpiod_ext]
+if GPIOD_WITH_TESTS:
+    extensions.append(gpiosim_ext)
+    extensions.append(procname_ext)
+
+setup(
+    name="gpiod",
+    url="https://git.kernel.org/pub/scm/libs/libgpiod/libgpiod.git",
+    packages=find_packages(exclude=["tests", "tests.*"]),
+    python_requires=">=3.9.0",
+    ext_modules=extensions,
+    cmdclass={"build_ext": build_ext, "sdist": sdist},
+    version=__version__,
+    author="Bartosz Golaszewski",
+    author_email="brgl@bgdev.pl",
+    description="Python bindings for libgpiod",
+    long_description=open("README.md", "r").read(),
+    long_description_content_type="text/markdown",
+    platforms=["linux"],
+    license="LGPLv2.1",
+)
diff --git a/bindings/python/tests/Makefile.am b/bindings/python/tests/Makefile.am
new file mode 100644 (file)
index 0000000..c89241e
--- /dev/null
@@ -0,0 +1,17 @@
+# SPDX-License-Identifier: LGPL-2.1-or-later
+# SPDX-FileCopyrightText: 2022 Bartosz Golaszewski <brgl@bgdev.pl>
+
+SUBDIRS = gpiosim procname
+
+EXTRA_DIST = \
+       helpers.py \
+       __init__.py \
+       __main__.py \
+       tests_chip_info.py \
+       tests_chip.py \
+       tests_edge_event.py \
+       tests_info_event.py \
+       tests_line_info.py \
+       tests_line_request.py \
+       tests_line_settings.py \
+       tests_module.py
diff --git a/bindings/python/tests/__init__.py b/bindings/python/tests/__init__.py
new file mode 100644 (file)
index 0000000..02f4e8d
--- /dev/null
@@ -0,0 +1,17 @@
+# SPDX-License-Identifier: LGPL-2.1-or-later
+# SPDX-FileCopyrightText: 2022 Bartosz Golaszewski <brgl@bgdev.pl>
+
+import os
+import unittest
+
+from distutils.version import LooseVersion
+
+required_kernel_version = LooseVersion("5.19.0")
+current_version = LooseVersion(os.uname().release.split("-")[0])
+
+if current_version < required_kernel_version:
+    raise NotImplementedError(
+        "linux kernel version must be at least {} - got {}".format(
+            required_kernel_version, current_version
+        )
+    )
diff --git a/bindings/python/tests/__main__.py b/bindings/python/tests/__main__.py
new file mode 100644 (file)
index 0000000..cc39c29
--- /dev/null
@@ -0,0 +1,20 @@
+#!/usr/bin/python3
+# SPDX-License-Identifier: GPL-2.0-or-later
+# SPDX-FileCopyrightText: 2022 Bartosz Golaszewski <brgl@bgdev.pl>
+
+import unittest
+
+from .tests_chip import *
+from .tests_chip_info import *
+from .tests_edge_event import *
+from .tests_info_event import *
+from .tests_line_info import *
+from .tests_line_settings import *
+from .tests_module import *
+from .tests_line_request import *
+
+from . import procname
+
+procname.set_process_name("python-gpiod")
+
+unittest.main()
diff --git a/bindings/python/tests/gpiosim/Makefile.am b/bindings/python/tests/gpiosim/Makefile.am
new file mode 100644 (file)
index 0000000..7004f3a
--- /dev/null
@@ -0,0 +1,7 @@
+# SPDX-License-Identifier: LGPL-2.1-or-later
+# SPDX-FileCopyrightText: 2022 Bartosz Golaszewski <brgl@bgdev.pl>
+
+EXTRA_DIST = \
+       chip.py \
+       ext.c \
+       __init__.py
diff --git a/bindings/python/tests/gpiosim/__init__.py b/bindings/python/tests/gpiosim/__init__.py
new file mode 100644 (file)
index 0000000..f65e413
--- /dev/null
@@ -0,0 +1,4 @@
+# SPDX-License-Identifier: GPL-2.0-or-later
+# SPDX-FileCopyrightText: 2022 Bartosz Golaszewski <brgl@bgdev.pl>
+
+from .chip import Chip
diff --git a/bindings/python/tests/gpiosim/chip.py b/bindings/python/tests/gpiosim/chip.py
new file mode 100644 (file)
index 0000000..6af883e
--- /dev/null
@@ -0,0 +1,65 @@
+# SPDX-License-Identifier: GPL-2.0-or-later
+# SPDX-FileCopyrightText: 2022 Bartosz Golaszewski <brgl@bgdev.pl>
+
+from . import _ext
+from enum import Enum
+from typing import Optional
+
+
+class Chip:
+    """
+    Represents a simulated GPIO chip.
+    """
+
+    class Pull(Enum):
+        DOWN = _ext.PULL_DOWN
+        UP = _ext.PULL_UP
+
+    class Value(Enum):
+        INACTIVE = _ext.VALUE_INACTIVE
+        ACTIVE = _ext.VALUE_ACTIVE
+
+    class Direction(Enum):
+        INPUT = _ext.DIRECTION_INPUT
+        OUTPUT_HIGH = _ext.DIRECTION_OUTPUT_HIGH
+        OUTPUT_LOW = _ext.DIRECTION_OUTPUT_LOW
+
+    def __init__(
+        self,
+        label: Optional[str] = None,
+        num_lines: Optional[int] = None,
+        line_names: Optional[dict[int, str]] = None,
+        hogs: Optional[dict[int, tuple[str, Direction]]] = None,
+    ):
+        self._chip = _ext.Chip()
+
+        if label:
+            self._chip.set_label(label)
+
+        if num_lines:
+            self._chip.set_num_lines(num_lines)
+
+        if line_names:
+            for off, name in line_names.items():
+                self._chip.set_line_name(off, name)
+
+        if hogs:
+            for off, (name, direction) in hogs.items():
+                self._chip.set_hog(off, name, direction.value)
+
+        self._chip.enable()
+
+    def get_value(self, offset: int) -> Value:
+        val = self._chip.get_value(offset)
+        return Chip.Value(val)
+
+    def set_pull(self, offset: int, pull: Pull) -> None:
+        self._chip.set_pull(offset, pull.value)
+
+    @property
+    def dev_path(self) -> str:
+        return self._chip.dev_path
+
+    @property
+    def name(self) -> str:
+        return self._chip.name
diff --git a/bindings/python/tests/gpiosim/ext.c b/bindings/python/tests/gpiosim/ext.c
new file mode 100644 (file)
index 0000000..272e6f7
--- /dev/null
@@ -0,0 +1,345 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// SPDX-FileCopyrightText: 2022 Bartosz Golaszewski <brgl@bgdev.pl>
+
+#include <gpiosim.h>
+#include <Python.h>
+
+struct module_const {
+       const char *name;
+       long val;
+};
+
+static const struct module_const module_constants[] = {
+       {
+               .name = "PULL_DOWN",
+               .val = GPIOSIM_PULL_DOWN,
+       },
+       {
+               .name = "PULL_UP",
+               .val = GPIOSIM_PULL_UP,
+       },
+       {
+               .name = "VALUE_INACTIVE",
+               .val = GPIOSIM_VALUE_INACTIVE,
+       },
+       {
+               .name = "VALUE_ACTIVE",
+               .val = GPIOSIM_VALUE_ACTIVE,
+       },
+       {
+               .name = "DIRECTION_INPUT",
+               .val = GPIOSIM_DIRECTION_INPUT,
+       },
+       {
+               .name = "DIRECTION_OUTPUT_HIGH",
+               .val = GPIOSIM_DIRECTION_OUTPUT_HIGH,
+       },
+       {
+               .name = "DIRECTION_OUTPUT_LOW",
+               .val = GPIOSIM_DIRECTION_OUTPUT_LOW,
+       },
+       { }
+};
+
+struct module_state {
+       struct gpiosim_ctx *sim_ctx;
+};
+
+static void free_module_state(void *mod)
+{
+       struct module_state *state = PyModule_GetState((PyObject *)mod);
+
+       if (state->sim_ctx)
+               gpiosim_ctx_unref(state->sim_ctx);
+}
+
+static PyModuleDef module_def = {
+       PyModuleDef_HEAD_INIT,
+       .m_name = "gpiosim._ext",
+       .m_size = sizeof(struct module_state),
+       .m_free = free_module_state,
+};
+
+typedef struct {
+       PyObject_HEAD
+       struct gpiosim_dev *dev;
+       struct gpiosim_bank *bank;
+} chip_object;
+
+static int chip_init(chip_object *self,
+                    PyObject *Py_UNUSED(ignored0),
+                    PyObject *Py_UNUSED(ignored1))
+{
+       struct module_state *state;
+       PyObject *mod;
+
+       mod = PyState_FindModule(&module_def);
+       if (!mod)
+               return -1;
+
+       state = PyModule_GetState(mod);
+
+       self->dev = gpiosim_dev_new(state->sim_ctx);
+       if (!self->dev) {
+               PyErr_SetFromErrno(PyExc_OSError);
+               return -1;
+       }
+
+       self->bank = gpiosim_bank_new(self->dev);
+       if (!self->bank) {
+               PyErr_SetFromErrno(PyExc_OSError);
+               return -1;
+       }
+
+       return 0;
+}
+
+static void chip_finalize(chip_object *self)
+{
+       if (self->bank)
+               gpiosim_bank_unref(self->bank);
+
+       if (self->dev) {
+               if (gpiosim_dev_is_live(self->dev))
+                       gpiosim_dev_disable(self->dev);
+
+               gpiosim_dev_unref(self->dev);
+       }
+}
+
+static void chip_dealloc(PyObject *self)
+{
+       int ret;
+
+       ret = PyObject_CallFinalizerFromDealloc(self);
+       if (ret < 0)
+               return;
+
+       PyObject_Del(self);
+}
+
+static PyObject *chip_dev_path(chip_object *self, void *Py_UNUSED(ignored))
+{
+       return PyUnicode_FromString(gpiosim_bank_get_dev_path(self->bank));
+}
+
+static PyObject *chip_name(chip_object *self, void *Py_UNUSED(ignored))
+{
+       return PyUnicode_FromString(gpiosim_bank_get_chip_name(self->bank));
+}
+
+static PyGetSetDef chip_getset[] = {
+       {
+               .name = "dev_path",
+               .get = (getter)chip_dev_path,
+       },
+       {
+               .name = "name",
+               .get = (getter)chip_name,
+       },
+       { }
+};
+
+static PyObject *chip_set_label(chip_object *self, PyObject *args)
+{
+       const char *label;
+       int ret;
+
+       ret = PyArg_ParseTuple(args, "s", &label);
+       if (!ret)
+               return NULL;
+
+       ret = gpiosim_bank_set_label(self->bank, label);
+       if (ret)
+               return PyErr_SetFromErrno(PyExc_OSError);
+
+       Py_RETURN_NONE;
+}
+
+static PyObject *chip_set_num_lines(chip_object *self, PyObject *args)
+{
+       unsigned int num_lines;
+       int ret;
+
+       ret = PyArg_ParseTuple(args, "I", &num_lines);
+       if (!ret)
+               return NULL;
+
+       ret = gpiosim_bank_set_num_lines(self->bank, num_lines);
+       if (ret)
+               return PyErr_SetFromErrno(PyExc_OSError);
+
+       Py_RETURN_NONE;
+}
+
+static PyObject *chip_set_line_name(chip_object *self, PyObject *args)
+{
+       unsigned int offset;
+       const char *name;
+       int ret;
+
+       ret = PyArg_ParseTuple(args, "Is", &offset, &name);
+       if (!ret)
+               return NULL;
+
+       ret = gpiosim_bank_set_line_name(self->bank, offset, name);
+       if (ret)
+               return PyErr_SetFromErrno(PyExc_OSError);
+
+       Py_RETURN_NONE;
+}
+
+static PyObject *chip_set_hog(chip_object *self, PyObject *args)
+{
+       unsigned int offset;
+       const char *name;
+       int ret, dir;
+
+       ret = PyArg_ParseTuple(args, "Isi", &offset, &name, &dir);
+       if (!ret)
+               return NULL;
+
+       ret = gpiosim_bank_hog_line(self->bank, offset, name, dir);
+       if (ret)
+               return PyErr_SetFromErrno(PyExc_OSError);
+
+       Py_RETURN_NONE;
+}
+
+static PyObject *chip_enable(chip_object *self, PyObject *Py_UNUSED(args))
+{
+       int ret;
+
+       ret = gpiosim_dev_enable(self->dev);
+       if (ret)
+               return PyErr_SetFromErrno(PyExc_OSError);
+
+       Py_RETURN_NONE;
+}
+
+static PyObject *chip_get_value(chip_object *self, PyObject *args)
+{
+       unsigned int offset;
+       int ret, val;
+
+       ret = PyArg_ParseTuple(args, "I", &offset);
+       if (!ret)
+               return NULL;
+
+       val = gpiosim_bank_get_value(self->bank, offset);
+       if (val < 0)
+               return PyErr_SetFromErrno(PyExc_OSError);
+
+       return PyLong_FromLong(val);
+}
+
+static PyObject *chip_set_pull(chip_object *self, PyObject *args)
+{
+       unsigned int offset;
+       int ret, pull;
+
+       ret = PyArg_ParseTuple(args, "II", &offset, &pull);
+       if (!ret)
+               return NULL;
+
+       ret = gpiosim_bank_set_pull(self->bank, offset, pull);
+       if (ret)
+               return PyErr_SetFromErrno(PyExc_OSError);
+
+       Py_RETURN_NONE;
+}
+
+static PyMethodDef chip_methods[] = {
+       {
+               .ml_name = "set_label",
+               .ml_meth = (PyCFunction)chip_set_label,
+               .ml_flags = METH_VARARGS,
+       },
+       {
+               .ml_name = "set_num_lines",
+               .ml_meth = (PyCFunction)chip_set_num_lines,
+               .ml_flags = METH_VARARGS,
+       },
+       {
+               .ml_name = "set_line_name",
+               .ml_meth = (PyCFunction)chip_set_line_name,
+               .ml_flags = METH_VARARGS,
+       },
+       {
+               .ml_name = "set_hog",
+               .ml_meth = (PyCFunction)chip_set_hog,
+               .ml_flags = METH_VARARGS,
+       },
+       {
+               .ml_name = "enable",
+               .ml_meth = (PyCFunction)chip_enable,
+               .ml_flags = METH_NOARGS,
+       },
+       {
+               .ml_name = "get_value",
+               .ml_meth = (PyCFunction)chip_get_value,
+               .ml_flags = METH_VARARGS,
+       },
+       {
+               .ml_name = "set_pull",
+               .ml_meth = (PyCFunction)chip_set_pull,
+               .ml_flags = METH_VARARGS,
+       },
+       { }
+};
+
+static PyTypeObject chip_type = {
+       PyVarObject_HEAD_INIT(NULL, 0)
+       .tp_name = "gpiosim.Chip",
+       .tp_basicsize = sizeof(chip_object),
+       .tp_flags = Py_TPFLAGS_DEFAULT,
+       .tp_new = PyType_GenericNew,
+       .tp_init = (initproc)chip_init,
+       .tp_finalize = (destructor)chip_finalize,
+       .tp_dealloc = (destructor)chip_dealloc,
+       .tp_methods = chip_methods,
+       .tp_getset = chip_getset,
+};
+
+PyMODINIT_FUNC PyInit__ext(void)
+{
+       const struct module_const *modconst;
+       struct module_state *state;
+       PyObject *module;
+       int ret;
+
+       module = PyModule_Create(&module_def);
+       if (!module)
+               return NULL;
+
+       ret = PyState_AddModule(module, &module_def);
+       if (ret) {
+               Py_DECREF(module);
+               return NULL;
+       }
+
+       state = PyModule_GetState(module);
+
+       state->sim_ctx = gpiosim_ctx_new();
+       if (!state->sim_ctx) {
+               Py_DECREF(module);
+               return PyErr_SetFromErrno(PyExc_OSError);
+       }
+
+       ret = PyModule_AddType(module, &chip_type);
+       if (ret) {
+               Py_DECREF(module);
+               return NULL;
+       }
+
+       for (modconst = module_constants; modconst->name; modconst++) {
+               ret = PyModule_AddIntConstant(module,
+                                             modconst->name, modconst->val);
+               if (ret) {
+                       Py_DECREF(module);
+                       return NULL;
+               }
+       }
+
+       return module;
+}
diff --git a/bindings/python/tests/helpers.py b/bindings/python/tests/helpers.py
new file mode 100644 (file)
index 0000000..f9a15e8
--- /dev/null
@@ -0,0 +1,16 @@
+# SPDX-License-Identifier: GPL-2.0-or-later
+# SPDX-FileCopyrightText: 2022 Bartosz Golaszewski <brgl@bgdev.pl>
+
+import os
+
+
+class LinkGuard:
+    def __init__(self, src, dst):
+        self.src = src
+        self.dst = dst
+
+    def __enter__(self):
+        os.symlink(self.src, self.dst)
+
+    def __exit__(self, type, val, tb):
+        os.unlink(self.dst)
diff --git a/bindings/python/tests/procname/Makefile.am b/bindings/python/tests/procname/Makefile.am
new file mode 100644 (file)
index 0000000..c4a8fd5
--- /dev/null
@@ -0,0 +1,6 @@
+# SPDX-License-Identifier: LGPL-2.1-or-later
+# SPDX-FileCopyrightText: 2022 Bartosz Golaszewski <brgl@bgdev.pl>
+
+EXTRA_DIST = \
+       ext.c \
+       __init__.py
diff --git a/bindings/python/tests/procname/__init__.py b/bindings/python/tests/procname/__init__.py
new file mode 100644 (file)
index 0000000..af6abdd
--- /dev/null
@@ -0,0 +1,4 @@
+# SPDX-License-Identifier: GPL-2.0-or-later
+# SPDX-FileCopyrightText: 2022 Bartosz Golaszewski <brgl@bgdev.pl>
+
+from ._ext import set_process_name
diff --git a/bindings/python/tests/procname/ext.c b/bindings/python/tests/procname/ext.c
new file mode 100644 (file)
index 0000000..bf7d2fe
--- /dev/null
@@ -0,0 +1,42 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// SPDX-FileCopyrightText: 2022 Bartosz Golaszewski <brgl@bgdev.pl>
+
+#include <Python.h>
+#include <sys/prctl.h>
+
+static PyObject *
+module_set_process_name(PyObject *Py_UNUSED(self), PyObject *args)
+{
+       const char *name;
+       int ret;
+
+       ret = PyArg_ParseTuple(args, "s", &name);
+       if (!ret)
+               return NULL;
+
+       ret = prctl(PR_SET_NAME, name);
+       if (ret)
+               return PyErr_SetFromErrno(PyExc_OSError);
+
+       Py_RETURN_NONE;
+}
+
+static PyMethodDef module_methods[] = {
+       {
+               .ml_name = "set_process_name",
+               .ml_meth = (PyCFunction)module_set_process_name,
+               .ml_flags = METH_VARARGS,
+       },
+       { }
+};
+
+static PyModuleDef module_def = {
+       PyModuleDef_HEAD_INIT,
+       .m_name = "procname._ext",
+       .m_methods = module_methods,
+};
+
+PyMODINIT_FUNC PyInit__ext(void)
+{
+       return PyModule_Create(&module_def);
+}
diff --git a/bindings/python/tests/tests_chip.py b/bindings/python/tests/tests_chip.py
new file mode 100644 (file)
index 0000000..8db4cdb
--- /dev/null
@@ -0,0 +1,231 @@
+# SPDX-License-Identifier: GPL-2.0-or-later
+# SPDX-FileCopyrightText: 2022 Bartosz Golaszewski <brgl@bgdev.pl>
+
+import errno
+import gpiod
+import os
+
+from . import gpiosim
+from .helpers import LinkGuard
+from unittest import TestCase
+
+
+class ChipConstructor(TestCase):
+    def test_open_existing_chip(self):
+        sim = gpiosim.Chip()
+
+        with gpiod.Chip(sim.dev_path):
+            pass
+
+    def test_open_existing_chip_with_keyword(self):
+        sim = gpiosim.Chip()
+
+        with gpiod.Chip(path=sim.dev_path):
+            pass
+
+    def test_open_chip_by_link(self):
+        link = "/tmp/gpiod-py-test-link.{}".format(os.getpid())
+        sim = gpiosim.Chip()
+
+        with LinkGuard(sim.dev_path, link):
+            with gpiod.Chip(link):
+                pass
+
+    def test_open_nonexistent_chip(self):
+        with self.assertRaises(OSError) as ex:
+            gpiod.Chip("/dev/nonexistent")
+
+        self.assertEqual(ex.exception.errno, errno.ENOENT)
+
+    def test_open_not_a_character_device(self):
+        with self.assertRaises(OSError) as ex:
+            gpiod.Chip("/tmp")
+
+        self.assertEqual(ex.exception.errno, errno.ENOTTY)
+
+    def test_open_not_a_gpio_device(self):
+        with self.assertRaises(OSError) as ex:
+            gpiod.Chip("/dev/null")
+
+        self.assertEqual(ex.exception.errno, errno.ENODEV)
+
+    def test_missing_path(self):
+        with self.assertRaises(TypeError):
+            gpiod.Chip()
+
+    def test_invalid_type_for_path(self):
+        with self.assertRaises(TypeError):
+            gpiod.Chip(4)
+
+
+class ChipBooleanConversion(TestCase):
+    def test_chip_bool(self):
+        sim = gpiosim.Chip()
+        chip = gpiod.Chip(sim.dev_path)
+        self.assertTrue(chip)
+        chip.close()
+        self.assertFalse(chip)
+
+
+class ChipProperties(TestCase):
+    def setUp(self):
+        self.sim = gpiosim.Chip()
+        self.chip = gpiod.Chip(self.sim.dev_path)
+
+    def tearDown(self):
+        self.chip.close()
+        self.sim = None
+
+    def test_get_chip_path(self):
+        self.assertEqual(self.sim.dev_path, self.chip.path)
+
+    def test_get_fd(self):
+        self.assertGreaterEqual(self.chip.fd, 0)
+
+    def test_properties_are_immutable(self):
+        with self.assertRaises(AttributeError):
+            self.chip.path = "foobar"
+
+        with self.assertRaises(AttributeError):
+            self.chip.fd = 4
+
+
+class ChipDevPathFromLink(TestCase):
+    def test_dev_path_open_by_link(self):
+        sim = gpiosim.Chip()
+        link = "/tmp/gpiod-py-test-link.{}".format(os.getpid())
+
+        with LinkGuard(sim.dev_path, link):
+            with gpiod.Chip(link) as chip:
+                self.assertEqual(chip.path, link)
+
+
+class ChipMapLine(TestCase):
+    def test_lookup_by_name_good(self):
+        sim = gpiosim.Chip(
+            num_lines=8, line_names={1: "foo", 2: "bar", 4: "baz", 5: "xyz"}
+        )
+
+        with gpiod.Chip(sim.dev_path) as chip:
+            self.assertEqual(chip.line_offset_from_id("baz"), 4)
+
+    def test_lookup_by_name_good_keyword_argument(self):
+        sim = gpiosim.Chip(
+            num_lines=8, line_names={1: "foo", 2: "bar", 4: "baz", 5: "xyz"}
+        )
+
+        with gpiod.Chip(sim.dev_path) as chip:
+            self.assertEqual(chip.line_offset_from_id(id="baz"), 4)
+
+    def test_lookup_bad_name(self):
+        sim = gpiosim.Chip(
+            num_lines=8, line_names={1: "foo", 2: "bar", 4: "baz", 5: "xyz"}
+        )
+
+        with gpiod.Chip(sim.dev_path) as chip:
+            with self.assertRaises(FileNotFoundError):
+                chip.line_offset_from_id("nonexistent")
+
+    def test_lookup_bad_offset(self):
+        sim = gpiosim.Chip()
+
+        with gpiod.Chip(sim.dev_path) as chip:
+            with self.assertRaises(ValueError):
+                chip.line_offset_from_id(4)
+
+    def test_lookup_bad_offset_as_string(self):
+        sim = gpiosim.Chip()
+
+        with gpiod.Chip(sim.dev_path) as chip:
+            with self.assertRaises(ValueError):
+                chip.line_offset_from_id("4")
+
+    def test_duplicate_names(self):
+        sim = gpiosim.Chip(
+            num_lines=8, line_names={1: "foo", 2: "bar", 4: "baz", 5: "bar"}
+        )
+
+        with gpiod.Chip(sim.dev_path) as chip:
+            self.assertEqual(chip.line_offset_from_id("bar"), 2)
+
+    def test_integer_offsets(self):
+        sim = gpiosim.Chip(num_lines=8, line_names={1: "foo", 2: "bar", 6: "baz"})
+
+        with gpiod.Chip(sim.dev_path) as chip:
+            self.assertEqual(chip.line_offset_from_id(4), 4)
+            self.assertEqual(chip.line_offset_from_id(1), 1)
+
+    def test_offsets_as_string(self):
+        sim = gpiosim.Chip(num_lines=8, line_names={1: "foo", 2: "bar", 7: "6"})
+
+        with gpiod.Chip(sim.dev_path) as chip:
+            self.assertEqual(chip.line_offset_from_id("2"), 2)
+            self.assertEqual(chip.line_offset_from_id("6"), 7)
+
+
+class ClosedChipCannotBeUsed(TestCase):
+    def test_close_chip_and_try_to_use_it(self):
+        sim = gpiosim.Chip(label="foobar")
+
+        chip = gpiod.Chip(sim.dev_path)
+        chip.close()
+
+        with self.assertRaises(gpiod.ChipClosedError):
+            chip.path
+
+    def test_close_chip_and_try_controlled_execution(self):
+        sim = gpiosim.Chip()
+
+        chip = gpiod.Chip(sim.dev_path)
+        chip.close()
+
+        with self.assertRaises(gpiod.ChipClosedError):
+            with chip:
+                chip.fd
+
+    def test_close_chip_twice(self):
+        sim = gpiosim.Chip(label="foobar")
+        chip = gpiod.Chip(sim.dev_path)
+        chip.close()
+
+        with self.assertRaises(gpiod.ChipClosedError):
+            chip.close()
+
+
+class StringRepresentation(TestCase):
+    def setUp(self):
+        self.sim = gpiosim.Chip(num_lines=4, label="foobar")
+        self.chip = gpiod.Chip(self.sim.dev_path)
+
+    def tearDown(self):
+        self.chip.close()
+        self.sim = None
+
+    def test_repr(self):
+        self.assertEqual(repr(self.chip), 'Chip("{}")'.format(self.sim.dev_path))
+
+    def test_str(self):
+        info = self.chip.get_info()
+        self.assertEqual(
+            str(self.chip),
+            '<Chip path="{}" fd={} info=<ChipInfo name="{}" label="foobar" num_lines=4>>'.format(
+                self.sim.dev_path, self.chip.fd, info.name
+            ),
+        )
+
+
+class StringRepresentationClosed(TestCase):
+    def setUp(self):
+        self.sim = gpiosim.Chip(num_lines=4, label="foobar")
+        self.chip = gpiod.Chip(self.sim.dev_path)
+
+    def tearDown(self):
+        self.sim = None
+
+    def test_repr_closed(self):
+        self.chip.close()
+        self.assertEqual(repr(self.chip), "<Chip CLOSED>")
+
+    def test_str_closed(self):
+        self.chip.close()
+        self.assertEqual(str(self.chip), "<Chip CLOSED>")
diff --git a/bindings/python/tests/tests_chip_info.py b/bindings/python/tests/tests_chip_info.py
new file mode 100644 (file)
index 0000000..d392ec3
--- /dev/null
@@ -0,0 +1,52 @@
+# SPDX-License-Identifier: GPL-2.0-or-later
+# SPDX-FileCopyrightText: 2022 Bartosz Golaszewski <brgl@bgdev.pl>
+
+import gpiod
+
+from . import gpiosim
+from unittest import TestCase
+
+
+class ChipInfoProperties(TestCase):
+    def setUp(self):
+        self.sim = gpiosim.Chip(label="foobar", num_lines=16)
+        self.chip = gpiod.Chip(self.sim.dev_path)
+        self.info = self.chip.get_info()
+
+    def tearDown(self):
+        self.info = None
+        self.chip.close()
+        self.chip = None
+        self.sim = None
+
+    def test_chip_info_name(self):
+        self.assertEqual(self.info.name, self.sim.name)
+
+    def test_chip_info_label(self):
+        self.assertEqual(self.info.label, "foobar")
+
+    def test_chip_info_num_lines(self):
+        self.assertEqual(self.info.num_lines, 16)
+
+    def test_chip_info_properties_are_immutable(self):
+        with self.assertRaises(AttributeError):
+            self.info.name = "foobar"
+
+        with self.assertRaises(AttributeError):
+            self.info.num_lines = 4
+
+        with self.assertRaises(AttributeError):
+            self.info.label = "foobar"
+
+
+class ChipInfoStringRepresentation(TestCase):
+    def test_chip_info_str(self):
+        sim = gpiosim.Chip(label="foobar", num_lines=16)
+
+        with gpiod.Chip(sim.dev_path) as chip:
+            info = chip.get_info()
+
+            self.assertEqual(
+                str(info),
+                '<ChipInfo name="{}" label="foobar" num_lines=16>'.format(sim.name),
+            )
diff --git a/bindings/python/tests/tests_edge_event.py b/bindings/python/tests/tests_edge_event.py
new file mode 100644 (file)
index 0000000..430b27d
--- /dev/null
@@ -0,0 +1,212 @@
+# SPDX-License-Identifier: GPL-2.0-or-later
+# SPDX-FileCopyrightText: 2022 Bartosz Golaszewski <brgl@bgdev.pl>
+
+import gpiod
+import time
+
+from . import gpiosim
+from datetime import timedelta
+from functools import partial
+from gpiod.line import Direction, Edge
+from threading import Thread
+from unittest import TestCase
+
+EventType = gpiod.EdgeEvent.Type
+Pull = gpiosim.Chip.Pull
+
+
+class EdgeEventWaitTimeout(TestCase):
+    def test_event_wait_timeout(self):
+        sim = gpiosim.Chip()
+
+        with gpiod.request_lines(
+            sim.dev_path,
+            {0: gpiod.LineSettings(edge_detection=Edge.BOTH)},
+        ) as req:
+            self.assertEqual(req.wait_edge_events(timedelta(microseconds=10000)), False)
+
+    def test_event_wait_timeout_float(self):
+        sim = gpiosim.Chip()
+
+        with gpiod.request_lines(
+            sim.dev_path,
+            {0: gpiod.LineSettings(edge_detection=Edge.BOTH)},
+        ) as req:
+            self.assertEqual(req.wait_edge_events(0.01), False)
+
+
+class EdgeEventInvalidConfig(TestCase):
+    def test_output_mode_and_edge_detection(self):
+        sim = gpiosim.Chip()
+
+        with self.assertRaises(ValueError):
+            gpiod.request_lines(
+                sim.dev_path,
+                {
+                    0: gpiod.LineSettings(
+                        direction=Direction.OUTPUT, edge_detection=Edge.BOTH
+                    )
+                },
+            )
+
+
+class WaitingForEdgeEvents(TestCase):
+    def setUp(self):
+        self.sim = gpiosim.Chip(num_lines=8)
+        self.thread = None
+
+    def tearDown(self):
+        if self.thread:
+            self.thread.join()
+            del self.thread
+        self.sim = None
+
+    def trigger_falling_and_rising_edge(self, offset):
+        time.sleep(0.05)
+        self.sim.set_pull(offset, Pull.UP)
+        time.sleep(0.05)
+        self.sim.set_pull(offset, Pull.DOWN)
+
+    def trigger_rising_edge_events_on_two_offsets(self, offset0, offset1):
+        time.sleep(0.05)
+        self.sim.set_pull(offset0, Pull.UP)
+        time.sleep(0.05)
+        self.sim.set_pull(offset1, Pull.UP)
+
+    def test_both_edge_events(self):
+        with gpiod.request_lines(
+            self.sim.dev_path, {2: gpiod.LineSettings(edge_detection=Edge.BOTH)}
+        ) as req:
+            self.thread = Thread(
+                target=partial(self.trigger_falling_and_rising_edge, 2)
+            )
+            self.thread.start()
+
+            self.assertTrue(req.wait_edge_events(timedelta(seconds=1)))
+            events = req.read_edge_events()
+            self.assertEqual(len(events), 1)
+            event = events[0]
+            self.assertEqual(event.event_type, EventType.RISING_EDGE)
+            self.assertEqual(event.line_offset, 2)
+            ts_rising = event.timestamp_ns
+
+            self.assertTrue(req.wait_edge_events(timedelta(seconds=1)))
+            events = req.read_edge_events()
+            self.assertEqual(len(events), 1)
+            event = events[0]
+            self.assertEqual(event.event_type, EventType.FALLING_EDGE)
+            self.assertEqual(event.line_offset, 2)
+            ts_falling = event.timestamp_ns
+
+            self.assertGreater(ts_falling, ts_rising)
+
+    def test_rising_edge_event(self):
+        with gpiod.request_lines(
+            self.sim.dev_path, {6: gpiod.LineSettings(edge_detection=Edge.RISING)}
+        ) as req:
+            self.thread = Thread(
+                target=partial(self.trigger_falling_and_rising_edge, 6)
+            )
+            self.thread.start()
+
+            self.assertTrue(req.wait_edge_events(timedelta(seconds=1)))
+            events = req.read_edge_events()
+            self.assertEqual(len(events), 1)
+            event = events[0]
+            self.assertEqual(event.event_type, EventType.RISING_EDGE)
+            self.assertEqual(event.line_offset, 6)
+
+            self.assertFalse(req.wait_edge_events(timedelta(microseconds=10000)))
+
+    def test_rising_edge_event(self):
+        with gpiod.request_lines(
+            self.sim.dev_path, {6: gpiod.LineSettings(edge_detection=Edge.FALLING)}
+        ) as req:
+            self.thread = Thread(
+                target=partial(self.trigger_falling_and_rising_edge, 6)
+            )
+            self.thread.start()
+
+            self.assertTrue(req.wait_edge_events(timedelta(seconds=1)))
+            events = req.read_edge_events()
+            self.assertEqual(len(events), 1)
+            event = events[0]
+            self.assertEqual(event.event_type, EventType.FALLING_EDGE)
+            self.assertEqual(event.line_offset, 6)
+
+            self.assertFalse(req.wait_edge_events(timedelta(microseconds=10000)))
+
+    def test_sequence_numbers(self):
+        with gpiod.request_lines(
+            self.sim.dev_path, {(2, 4): gpiod.LineSettings(edge_detection=Edge.BOTH)}
+        ) as req:
+            self.thread = Thread(
+                target=partial(self.trigger_rising_edge_events_on_two_offsets, 2, 4)
+            )
+            self.thread.start()
+
+            self.assertTrue(req.wait_edge_events(timedelta(seconds=1)))
+            events = req.read_edge_events()
+            self.assertEqual(len(events), 1)
+            event = events[0]
+            self.assertEqual(event.event_type, EventType.RISING_EDGE)
+            self.assertEqual(event.line_offset, 2)
+            self.assertEqual(event.global_seqno, 1)
+            self.assertEqual(event.line_seqno, 1)
+
+            self.assertTrue(req.wait_edge_events(timedelta(seconds=1)))
+            events = req.read_edge_events()
+            self.assertEqual(len(events), 1)
+            event = events[0]
+            self.assertEqual(event.event_type, EventType.RISING_EDGE)
+            self.assertEqual(event.line_offset, 4)
+            self.assertEqual(event.global_seqno, 2)
+            self.assertEqual(event.line_seqno, 1)
+
+
+class ReadingMultipleEdgeEvents(TestCase):
+    def setUp(self):
+        self.sim = gpiosim.Chip(num_lines=8)
+        self.request = gpiod.request_lines(
+            self.sim.dev_path, {1: gpiod.LineSettings(edge_detection=Edge.BOTH)}
+        )
+        self.line_seqno = 1
+        self.global_seqno = 1
+        self.sim.set_pull(1, Pull.UP)
+        time.sleep(0.05)
+        self.sim.set_pull(1, Pull.DOWN)
+        time.sleep(0.05)
+        self.sim.set_pull(1, Pull.UP)
+        time.sleep(0.05)
+
+    def tearDown(self):
+        self.request.release()
+        del self.request
+        del self.sim
+
+    def test_read_multiple_events(self):
+        self.assertTrue(self.request.wait_edge_events(timedelta(seconds=1)))
+        events = self.request.read_edge_events()
+        self.assertEqual(len(events), 3)
+
+        for event in events:
+            self.assertEqual(event.line_offset, 1)
+            self.assertEqual(event.line_seqno, self.line_seqno)
+            self.assertEqual(event.global_seqno, self.global_seqno)
+            self.line_seqno += 1
+            self.global_seqno += 1
+
+
+class EdgeEventStringRepresentation(TestCase):
+    def test_edge_event_str(self):
+        sim = gpiosim.Chip()
+
+        with gpiod.request_lines(
+            path=sim.dev_path, config={0: gpiod.LineSettings(edge_detection=Edge.BOTH)}
+        ) as req:
+            sim.set_pull(0, Pull.UP)
+            event = req.read_edge_events()[0]
+            self.assertRegex(
+                str(event),
+                "<EdgeEvent type=Type\.RISING_EDGE timestamp_ns=[0-9]+ line_offset=0 global_seqno=1 line_seqno=1>",
+            )
diff --git a/bindings/python/tests/tests_info_event.py b/bindings/python/tests/tests_info_event.py
new file mode 100644 (file)
index 0000000..6bb09d5
--- /dev/null
@@ -0,0 +1,189 @@
+# SPDX-License-Identifier: GPL-2.0-or-later
+# SPDX-FileCopyrightText: 2022 Bartosz Golaszewski <brgl@bgdev.pl>
+
+import datetime
+import errno
+import gpiod
+import threading
+import time
+import unittest
+
+from . import gpiosim
+from dataclasses import FrozenInstanceError
+from functools import partial
+from gpiod.line import Direction
+from unittest import TestCase
+
+EventType = gpiod.InfoEvent.Type
+
+
+class InfoEventDataclassBehavior(TestCase):
+    def test_info_event_props_are_frozen(self):
+        sim = gpiosim.Chip()
+
+        with gpiod.Chip(sim.dev_path) as chip:
+            chip.watch_line_info(0)
+            with chip.request_lines(config={0: None}) as request:
+                self.assertTrue(chip.wait_info_event(datetime.timedelta(seconds=1)))
+                event = chip.read_info_event()
+
+                with self.assertRaises(FrozenInstanceError):
+                    event.event_type = 4
+
+                with self.assertRaises(FrozenInstanceError):
+                    event.timestamp_ns = 4
+
+                with self.assertRaises(FrozenInstanceError):
+                    event.line_info = 4
+
+
+def request_reconfigure_release_line(chip_path, offset):
+    time.sleep(0.1)
+    with gpiod.request_lines(chip_path, config={offset: None}) as request:
+        time.sleep(0.1)
+        request.reconfigure_lines(
+            config={offset: gpiod.LineSettings(direction=Direction.OUTPUT)}
+        )
+        time.sleep(0.1)
+
+
+class WatchingInfoEventWorks(TestCase):
+    def setUp(self):
+        self.sim = gpiosim.Chip(num_lines=8, line_names={4: "foobar"})
+        self.chip = gpiod.Chip(self.sim.dev_path)
+        self.thread = None
+
+    def tearDown(self):
+        if self.thread:
+            self.thread.join()
+            self.thread = None
+
+        self.chip.close()
+        self.chip = None
+        self.sim = None
+
+    def test_watch_line_info_returns_line_info(self):
+        info = self.chip.watch_line_info(7)
+        self.assertEqual(info.offset, 7)
+
+    def test_watch_line_info_keyword_argument(self):
+        info = self.chip.watch_line_info(line=7)
+
+    def test_watch_line_info_offset_out_of_range(self):
+        with self.assertRaises(ValueError):
+            self.chip.watch_line_info(8)
+
+    def test_watch_line_info_no_arguments(self):
+        with self.assertRaises(TypeError):
+            self.chip.watch_line_info()
+
+    def test_watch_line_info_by_line_name(self):
+        self.chip.watch_line_info("foobar")
+
+    def test_watch_line_info_invalid_argument_type(self):
+        with self.assertRaises(TypeError):
+            self.chip.watch_line_info(None)
+
+    def test_wait_for_event_timeout(self):
+        info = self.chip.watch_line_info(7)
+        self.assertFalse(
+            self.chip.wait_info_event(datetime.timedelta(microseconds=10000))
+        )
+
+    def test_request_reconfigure_release_events(self):
+        info = self.chip.watch_line_info(7)
+        self.assertEqual(info.direction, Direction.INPUT)
+
+        self.thread = threading.Thread(
+            target=partial(request_reconfigure_release_line, self.sim.dev_path, 7)
+        )
+        self.thread.start()
+
+        self.assertTrue(self.chip.wait_info_event(datetime.timedelta(seconds=1)))
+        event = self.chip.read_info_event()
+        self.assertEqual(event.event_type, EventType.LINE_REQUESTED)
+        self.assertEqual(event.line_info.offset, 7)
+        self.assertEqual(event.line_info.direction, Direction.INPUT)
+        ts_req = event.timestamp_ns
+
+        # Check that we can use a float directly instead of datetime.timedelta.
+        self.assertTrue(self.chip.wait_info_event(1.0))
+        event = self.chip.read_info_event()
+        self.assertEqual(event.event_type, EventType.LINE_CONFIG_CHANGED)
+        self.assertEqual(event.line_info.offset, 7)
+        self.assertEqual(event.line_info.direction, Direction.OUTPUT)
+        ts_rec = event.timestamp_ns
+
+        self.assertTrue(self.chip.wait_info_event(datetime.timedelta(seconds=1)))
+        event = self.chip.read_info_event()
+        self.assertEqual(event.event_type, EventType.LINE_RELEASED)
+        self.assertEqual(event.line_info.offset, 7)
+        self.assertEqual(event.line_info.direction, Direction.OUTPUT)
+        ts_rel = event.timestamp_ns
+
+        # No more events.
+        self.assertFalse(
+            self.chip.wait_info_event(datetime.timedelta(microseconds=10000))
+        )
+
+        # Check timestamps are really monotonic.
+        self.assertGreater(ts_rel, ts_rec)
+        self.assertGreater(ts_rec, ts_req)
+
+
+class UnwatchingLineInfo(TestCase):
+    def setUp(self):
+        self.sim = gpiosim.Chip(num_lines=8, line_names={4: "foobar"})
+        self.chip = gpiod.Chip(self.sim.dev_path)
+
+    def tearDown(self):
+        self.chip.close()
+        self.chip = None
+        self.sim = None
+
+    def test_unwatch_line_info(self):
+        self.chip.watch_line_info(0)
+        with self.chip.request_lines(config={0: None}) as request:
+            self.assertTrue(self.chip.wait_info_event(datetime.timedelta(seconds=1)))
+            event = self.chip.read_info_event()
+            self.assertEqual(event.event_type, EventType.LINE_REQUESTED)
+            self.chip.unwatch_line_info(0)
+
+        self.assertFalse(
+            self.chip.wait_info_event(datetime.timedelta(microseconds=10000))
+        )
+
+    def test_unwatch_not_watched_line(self):
+        with self.assertRaises(OSError) as ex:
+            self.chip.unwatch_line_info(2)
+
+        self.assertEqual(ex.exception.errno, errno.EBUSY)
+
+    def test_unwatch_line_info_no_argument(self):
+        with self.assertRaises(TypeError):
+            self.chip.unwatch_line_info()
+
+    def test_unwatch_line_info_by_line_name(self):
+        self.chip.watch_line_info(4)
+        with self.chip.request_lines(config={4: None}) as request:
+            self.assertIsNotNone(self.chip.read_info_event())
+            self.chip.unwatch_line_info("foobar")
+
+        self.assertFalse(
+            self.chip.wait_info_event(datetime.timedelta(microseconds=10000))
+        )
+
+
+class InfoEventStringRepresentation(TestCase):
+    def test_info_event_str(self):
+        sim = gpiosim.Chip()
+
+        with gpiod.Chip(sim.dev_path) as chip:
+            chip.watch_line_info(0)
+            with chip.request_lines(config={0: None}) as request:
+                self.assertTrue(chip.wait_info_event(datetime.timedelta(seconds=1)))
+                event = chip.read_info_event()
+                self.assertRegex(
+                    str(event),
+                    '<InfoEvent type=Type\.LINE_REQUESTED timestamp_ns=[0-9]+ line_info=<LineInfo offset=0 name="None" used=True consumer="\?" direction=Direction\.INPUT active_low=False bias=Bias\.UNKNOWN drive=Drive\.PUSH_PULL edge_detection=Edge\.NONE event_clock=Clock\.MONOTONIC debounced=False debounce_period=0:00:00>>',
+                )
diff --git a/bindings/python/tests/tests_line_info.py b/bindings/python/tests/tests_line_info.py
new file mode 100644 (file)
index 0000000..2779e7a
--- /dev/null
@@ -0,0 +1,101 @@
+# SPDX-License-Identifier: GPL-2.0-or-later
+# SPDX-FileCopyrightText: 2022 Bartosz Golaszewski <brgl@bgdev.pl>
+
+import errno
+import gpiod
+import unittest
+
+from . import gpiosim
+from gpiod.line import Direction, Bias, Drive, Clock
+
+HogDir = gpiosim.Chip.Direction
+
+
+class GetLineInfo(unittest.TestCase):
+    def setUp(self):
+        self.sim = gpiosim.Chip(
+            num_lines=4,
+            line_names={0: "foobar"},
+        )
+
+        self.chip = gpiod.Chip(self.sim.dev_path)
+
+    def tearDown(self):
+        self.chip.close()
+        self.chip = None
+        self.sim = None
+
+    def test_get_line_info_by_offset(self):
+        self.chip.get_line_info(0)
+
+    def test_get_line_info_by_offset_keyword(self):
+        self.chip.get_line_info(line=0)
+
+    def test_get_line_info_by_name(self):
+        self.chip.get_line_info("foobar")
+
+    def test_get_line_info_by_name_keyword(self):
+        self.chip.get_line_info(line="foobar")
+
+    def test_get_line_info_by_offset_string(self):
+        self.chip.get_line_info("2")
+
+    def test_offset_out_of_range(self):
+        with self.assertRaises(ValueError) as ex:
+            self.chip.get_line_info(4)
+
+    def test_no_offset(self):
+        with self.assertRaises(TypeError):
+            self.chip.get_line_info()
+
+
+class LinePropertiesCanBeRead(unittest.TestCase):
+    def test_basic_properties(self):
+        sim = gpiosim.Chip(
+            num_lines=8,
+            line_names={1: "foo", 2: "bar", 4: "baz", 5: "xyz"},
+            hogs={3: ("hog3", HogDir.OUTPUT_HIGH), 4: ("hog4", HogDir.OUTPUT_LOW)},
+        )
+
+        with gpiod.Chip(sim.dev_path) as chip:
+            info4 = chip.get_line_info(4)
+            info6 = chip.get_line_info(6)
+
+            self.assertEqual(info4.offset, 4)
+            self.assertEqual(info4.name, "baz")
+            self.assertTrue(info4.used)
+            self.assertEqual(info4.consumer, "hog4")
+            self.assertEqual(info4.direction, Direction.OUTPUT)
+            self.assertFalse(info4.active_low)
+            self.assertEqual(info4.bias, Bias.UNKNOWN)
+            self.assertEqual(info4.drive, Drive.PUSH_PULL)
+            self.assertEqual(info4.event_clock, Clock.MONOTONIC)
+            self.assertFalse(info4.debounced)
+            self.assertEqual(info4.debounce_period.total_seconds(), 0.0)
+
+            self.assertEqual(info6.offset, 6)
+            self.assertEqual(info6.name, None)
+            self.assertFalse(info6.used)
+            self.assertEqual(info6.consumer, None)
+            self.assertEqual(info6.direction, Direction.INPUT)
+            self.assertFalse(info6.active_low)
+            self.assertEqual(info6.bias, Bias.UNKNOWN)
+            self.assertEqual(info6.drive, Drive.PUSH_PULL)
+            self.assertEqual(info6.event_clock, Clock.MONOTONIC)
+            self.assertFalse(info6.debounced)
+            self.assertEqual(info6.debounce_period.total_seconds(), 0.0)
+
+
+class LineInfoStringRepresentation(unittest.TestCase):
+    def test_line_info_str(self):
+        sim = gpiosim.Chip(
+            line_names={0: "foo"}, hogs={0: ("hogger", HogDir.OUTPUT_HIGH)}
+        )
+
+        with gpiod.Chip(sim.dev_path) as chip:
+            info = chip.get_line_info(0)
+
+            self.assertEqual(
+                str(info),
+                '<LineInfo offset=0 name="foo" used=True consumer="hogger" direction=Direction.OUTPUT active_low=False bias=Bias.UNKNOWN drive=Drive.PUSH_PULL edge_detection=Edge.NONE event_clock=Clock.MONOTONIC debounced=False debounce_period=0:00:00>',
+            )
diff --git a/bindings/python/tests/tests_line_request.py b/bindings/python/tests/tests_line_request.py
new file mode 100644 (file)
index 0000000..f99b93d
--- /dev/null
@@ -0,0 +1,544 @@
+# SPDX-License-Identifier: GPL-2.0-or-later
+# SPDX-FileCopyrightText: 2022 Bartosz Golaszewski <brgl@bgdev.pl>
+
+import errno
+import gpiod
+
+from . import gpiosim
+from gpiod.line import Direction, Edge, Value
+from unittest import TestCase
+
+Pull = gpiosim.Chip.Pull
+SimVal = gpiosim.Chip.Value
+
+
+class ChipLineRequestsBehaveCorrectlyWithInvalidArguments(TestCase):
+    def setUp(self):
+        self.sim = gpiosim.Chip(num_lines=8)
+        self.chip = gpiod.Chip(self.sim.dev_path)
+
+    def tearDown(self):
+        self.chip.close()
+        del self.chip
+        del self.sim
+
+    def test_passing_invalid_types_as_configs(self):
+        with self.assertRaises(AttributeError):
+            self.chip.request_lines("foobar")
+
+        with self.assertRaises(AttributeError):
+            self.chip.request_lines(None, "foobar")
+
+    def test_offset_out_of_range(self):
+        with self.assertRaises(ValueError):
+            self.chip.request_lines(config={(1, 0, 4, 8): None})
+
+    def test_line_name_not_found(self):
+        with self.assertRaises(FileNotFoundError):
+            self.chip.request_lines(config={"foo": None})
+
+    def test_request_no_arguments(self):
+        with self.assertRaises(TypeError):
+            self.chip.request_lines()
+
+
+class ModuleLineRequestsBehaveCorrectlyWithInvalidArguments(TestCase):
+    def setUp(self):
+        self.sim = gpiosim.Chip(num_lines=8)
+
+    def tearDown(self):
+        del self.sim
+
+    def test_passing_invalid_types_as_configs(self):
+        with self.assertRaises(AttributeError):
+            gpiod.request_lines(self.sim.dev_path, "foobar")
+
+        with self.assertRaises(AttributeError):
+            gpiod.request_lines(self.sim.dev_path, None, "foobar")
+
+    def test_offset_out_of_range(self):
+        with self.assertRaises(ValueError):
+            gpiod.request_lines(self.sim.dev_path, config={(1, 0, 4, 8): None})
+
+    def test_line_name_not_found(self):
+        with self.assertRaises(FileNotFoundError):
+            gpiod.request_lines(self.sim.dev_path, config={"foo": None})
+
+    def test_request_no_arguments(self):
+        with self.assertRaises(TypeError):
+            gpiod.request_lines()
+
+
+class ChipLineRequestWorks(TestCase):
+    def setUp(self):
+        self.sim = gpiosim.Chip(num_lines=8, line_names={5: "foo", 7: "bar"})
+        self.chip = gpiod.Chip(self.sim.dev_path)
+
+    def tearDown(self):
+        self.chip.close()
+        del self.chip
+        del self.sim
+
+    def test_request_with_positional_arguments(self):
+        with self.chip.request_lines({(0, 5, 3, 1): None}, "foobar", 32) as req:
+            self.assertEqual(req.offsets, [0, 5, 3, 1])
+            self.assertEqual(self.chip.get_line_info(0).consumer, "foobar")
+
+    def test_request_with_keyword_arguments(self):
+        with self.chip.request_lines(
+            config={(0, 5, 6): None},
+            consumer="foobar",
+            event_buffer_size=16,
+        ) as req:
+            self.assertEqual(req.offsets, [0, 5, 6])
+            self.assertEqual(self.chip.get_line_info(0).consumer, "foobar")
+
+    def test_request_single_offset_as_int(self):
+        with self.chip.request_lines(config={4: None}) as req:
+            self.assertEqual(req.offsets, [4])
+
+    def test_request_single_offset_as_tuple(self):
+        with self.chip.request_lines(config={(4): None}) as req:
+            self.assertEqual(req.offsets, [4])
+
+    def test_request_by_name(self):
+        with self.chip.request_lines(config={(1, 2, "foo", "bar"): None}) as req:
+            self.assertEqual(req.offsets, [1, 2, 5, 7])
+
+    def test_request_single_line_by_name(self):
+        with self.chip.request_lines(config={"foo": None}) as req:
+            self.assertEqual(req.offsets, [5])
+
+
+class ModuleLineRequestWorks(TestCase):
+    def setUp(self):
+        self.sim = gpiosim.Chip(num_lines=8, line_names={5: "foo", 7: "bar"})
+
+    def tearDown(self):
+        del self.sim
+
+    def test_request_with_positional_arguments(self):
+        with gpiod.request_lines(
+            self.sim.dev_path, {(0, 5, 3, 1): None}, "foobar", 32
+        ) as req:
+            self.assertEqual(req.offsets, [0, 5, 3, 1])
+            with gpiod.Chip(self.sim.dev_path) as chip:
+                self.assertEqual(chip.get_line_info(5).consumer, "foobar")
+
+    def test_request_with_keyword_arguments(self):
+        with gpiod.request_lines(
+            path=self.sim.dev_path,
+            config={(0, 5, 6): None},
+            consumer="foobar",
+            event_buffer_size=16,
+        ) as req:
+            self.assertEqual(req.offsets, [0, 5, 6])
+            with gpiod.Chip(self.sim.dev_path) as chip:
+                self.assertEqual(chip.get_line_info(5).consumer, "foobar")
+
+    def test_request_single_offset_as_int(self):
+        with gpiod.request_lines(path=self.sim.dev_path, config={4: None}) as req:
+            self.assertEqual(req.offsets, [4])
+
+    def test_request_single_offset_as_tuple(self):
+        with gpiod.request_lines(path=self.sim.dev_path, config={(4): None}) as req:
+            self.assertEqual(req.offsets, [4])
+
+    def test_request_by_name(self):
+        with gpiod.request_lines(
+            self.sim.dev_path, {(1, 2, "foo", "bar"): None}
+        ) as req:
+            self.assertEqual(req.offsets, [1, 2, 5, 7])
+
+
+class LineRequestGettingValues(TestCase):
+    def setUp(self):
+        self.sim = gpiosim.Chip(num_lines=8)
+        self.req = gpiod.request_lines(
+            self.sim.dev_path,
+            {(0, 1, 2, 3): gpiod.LineSettings(direction=Direction.INPUT)},
+        )
+
+    def tearDown(self):
+        self.req.release()
+        del self.req
+        del self.sim
+
+    def test_get_single_value(self):
+        self.sim.set_pull(1, Pull.UP)
+
+        self.assertEqual(self.req.get_values([1]), [Value.ACTIVE])
+
+    def test_get_single_value_helper(self):
+        self.sim.set_pull(1, Pull.UP)
+
+        self.assertEqual(self.req.get_value(1), Value.ACTIVE)
+
+    def test_get_values_for_subset_of_lines(self):
+        self.sim.set_pull(0, Pull.UP)
+        self.sim.set_pull(1, Pull.DOWN)
+        self.sim.set_pull(3, Pull.UP)
+
+        self.assertEqual(
+            self.req.get_values([0, 1, 3]), [Value.ACTIVE, Value.INACTIVE, Value.ACTIVE]
+        )
+
+    def test_get_all_values(self):
+        self.sim.set_pull(0, Pull.DOWN)
+        self.sim.set_pull(1, Pull.UP)
+        self.sim.set_pull(2, Pull.UP)
+        self.sim.set_pull(3, Pull.UP)
+
+        self.assertEqual(
+            self.req.get_values(),
+            [Value.INACTIVE, Value.ACTIVE, Value.ACTIVE, Value.ACTIVE],
+        )
+
+    def test_get_values_invalid_offset(self):
+        with self.assertRaises(ValueError):
+            self.req.get_values([9])
+
+    def test_get_values_invalid_argument_type(self):
+        with self.assertRaises(TypeError):
+            self.req.get_values(True)
+
+
+class LineRequestGettingValuesByName(TestCase):
+    def setUp(self):
+        self.sim = gpiosim.Chip(num_lines=4, line_names={2: "foo", 3: "bar", 1: "baz"})
+        self.req = gpiod.request_lines(
+            self.sim.dev_path,
+            {(0, "baz", "bar", "foo"): gpiod.LineSettings(direction=Direction.INPUT)},
+        )
+
+    def tearDown(self):
+        self.req.release()
+        del self.req
+        del self.sim
+
+    def test_get_values_by_name(self):
+        self.sim.set_pull(1, Pull.UP)
+        self.sim.set_pull(2, Pull.DOWN)
+        self.sim.set_pull(3, Pull.UP)
+
+        self.assertEqual(
+            self.req.get_values(["foo", "bar", 1]),
+            [Value.INACTIVE, Value.ACTIVE, Value.ACTIVE],
+        )
+
+    def test_get_values_by_bad_name(self):
+        with self.assertRaises(ValueError):
+            self.req.get_values(["xyz"])
+
+
+class LineRequestSettingValues(TestCase):
+    def setUp(self):
+        self.sim = gpiosim.Chip(num_lines=8)
+        self.req = gpiod.request_lines(
+            self.sim.dev_path,
+            {(0, 1, 2, 3): gpiod.LineSettings(direction=Direction.OUTPUT)},
+        )
+
+    def tearDown(self):
+        self.req.release()
+        del self.req
+        del self.sim
+
+    def test_set_single_value(self):
+        self.req.set_values({1: Value.ACTIVE})
+        self.assertEqual(self.sim.get_value(1), SimVal.ACTIVE)
+
+    def test_set_single_value_helper(self):
+        self.req.set_value(1, Value.ACTIVE)
+        self.assertEqual(self.sim.get_value(1), SimVal.ACTIVE)
+
+    def test_set_values_for_subset_of_lines(self):
+        self.req.set_values({0: Value.ACTIVE, 1: Value.INACTIVE, 3: Value.ACTIVE})
+
+        self.assertEqual(self.sim.get_value(0), SimVal.ACTIVE)
+        self.assertEqual(self.sim.get_value(1), SimVal.INACTIVE)
+        self.assertEqual(self.sim.get_value(3), SimVal.ACTIVE)
+
+    def test_set_values_invalid_offset(self):
+        with self.assertRaises(ValueError):
+            self.req.set_values({9: Value.ACTIVE})
+
+
+class LineRequestSettingValuesByName(TestCase):
+    def setUp(self):
+        self.sim = gpiosim.Chip(num_lines=4, line_names={2: "foo", 3: "bar", 1: "baz"})
+        self.req = gpiod.request_lines(
+            self.sim.dev_path,
+            {(0, "baz", "bar", "foo"): gpiod.LineSettings(direction=Direction.OUTPUT)},
+        )
+
+    def tearDown(self):
+        self.req.release()
+        del self.req
+        del self.sim
+
+    def test_set_values_by_name(self):
+        self.req.set_values(
+            {"foo": Value.INACTIVE, "bar": Value.ACTIVE, 1: Value.ACTIVE}
+        )
+
+        self.assertEqual(self.sim.get_value(2), SimVal.INACTIVE)
+        self.assertEqual(self.sim.get_value(1), SimVal.ACTIVE)
+        self.assertEqual(self.sim.get_value(3), SimVal.ACTIVE)
+
+    def test_set_values_by_bad_name(self):
+        with self.assertRaises(ValueError):
+            self.req.set_values({"xyz": Value.ACTIVE})
+
+
+class LineRequestComplexConfig(TestCase):
+    def test_complex_config(self):
+        sim = gpiosim.Chip(num_lines=8)
+
+        with gpiod.Chip(sim.dev_path) as chip:
+            with chip.request_lines(
+                config={
+                    (0, 2, 4): gpiod.LineSettings(
+                        direction=Direction.OUTPUT, output_value=Value.ACTIVE
+                    ),
+                    (1, 3, 5): gpiod.LineSettings(
+                        direction=Direction.INPUT, edge_detection=Edge.BOTH
+                    ),
+                },
+            ) as req:
+                self.assertEqual(chip.get_line_info(2).direction, Direction.OUTPUT)
+                self.assertEqual(chip.get_line_info(3).edge_detection, Edge.BOTH)
+
+
+class RepeatingLinesInRequestConfig(TestCase):
+    def setUp(self):
+        self.sim = gpiosim.Chip(num_lines=4, line_names={0: "foo", 2: "bar"})
+        self.chip = gpiod.Chip(self.sim.dev_path)
+
+    def tearDown(self):
+        self.chip.close()
+        del self.chip
+        del self.sim
+
+    def test_offsets_repeating_within_the_same_tuple(self):
+        with self.assertRaises(ValueError):
+            self.chip.request_lines({(0, 1, 2, 1): None})
+
+    def test_offsets_repeating_in_different_tuples(self):
+        with self.assertRaises(ValueError):
+            self.chip.request_lines({(0, 1, 2): None, (3, 4, 0): None})
+
+    def test_offset_and_name_conflict_in_the_same_tuple(self):
+        with self.assertRaises(ValueError):
+            self.chip.request_lines({(2, "bar"): None})
+
+    def test_offset_and_name_conflict_in_different_tuples(self):
+        with self.assertRaises(ValueError):
+            self.chip.request_lines({(0, 1, 2): None, (4, 5, "bar"): None})
+
+
+class LineRequestPropertiesWork(TestCase):
+    def setUp(self):
+        self.sim = gpiosim.Chip(num_lines=16, line_names={0: "foo", 2: "bar", 5: "baz"})
+
+    def tearDown(self):
+        del self.sim
+
+    def test_property_fd(self):
+        with gpiod.request_lines(
+            self.sim.dev_path,
+            config={
+                0: gpiod.LineSettings(
+                    direction=Direction.INPUT, edge_detection=Edge.BOTH
+                )
+            },
+        ) as req:
+            self.assertGreaterEqual(req.fd, 0)
+
+    def test_property_num_lines(self):
+        with gpiod.request_lines(
+            self.sim.dev_path, config={(0, 2, 3, 5, 6, 8, 12): None}
+        ) as req:
+            self.assertEqual(req.num_lines, 7)
+
+    def test_property_offsets(self):
+        with gpiod.request_lines(
+            self.sim.dev_path, config={(1, 6, 12, 4): None}
+        ) as req:
+            self.assertEqual(req.offsets, [1, 6, 12, 4])
+
+    def test_property_lines(self):
+        with gpiod.request_lines(
+            self.sim.dev_path, config={("foo", 1, "bar", 4, "baz"): None}
+        ) as req:
+            self.assertEqual(req.lines, ["foo", 1, "bar", 4, "baz"])
+
+
+class LineRequestConsumerString(TestCase):
+    def setUp(self):
+        self.sim = gpiosim.Chip(num_lines=4)
+        self.chip = gpiod.Chip(self.sim.dev_path)
+
+    def tearDown(self):
+        self.chip.close()
+        del self.chip
+        del self.sim
+
+    def test_custom_consumer(self):
+        with self.chip.request_lines(
+            consumer="foobar", config={(2, 3): None}
+        ) as request:
+            info = self.chip.get_line_info(2)
+            self.assertEqual(info.consumer, "foobar")
+
+    def test_empty_consumer(self):
+        with self.chip.request_lines(consumer="", config={(2, 3): None}) as request:
+            info = self.chip.get_line_info(2)
+            self.assertEqual(info.consumer, "?")
+
+    def test_default_consumer(self):
+        with self.chip.request_lines(config={(2, 3): None}) as request:
+            info = self.chip.get_line_info(2)
+            self.assertEqual(info.consumer, "?")
+
+
+class LineRequestSetOutputValues(TestCase):
+    def setUp(self):
+        self.sim = gpiosim.Chip(
+            num_lines=4, line_names={0: "foo", 1: "bar", 2: "baz", 3: "xyz"}
+        )
+
+    def tearDown(self):
+        del self.sim
+
+    def test_request_with_globally_set_output_values(self):
+        with gpiod.request_lines(
+            self.sim.dev_path,
+            config={(0, 1, 2, 3): gpiod.LineSettings(direction=Direction.OUTPUT)},
+            output_values={
+                0: Value.ACTIVE,
+                1: Value.INACTIVE,
+                2: Value.ACTIVE,
+                3: Value.INACTIVE,
+            },
+        ) as request:
+            self.assertEqual(self.sim.get_value(0), SimVal.ACTIVE)
+            self.assertEqual(self.sim.get_value(1), SimVal.INACTIVE)
+            self.assertEqual(self.sim.get_value(2), SimVal.ACTIVE)
+            self.assertEqual(self.sim.get_value(3), SimVal.INACTIVE)
+
+    def test_request_with_globally_set_output_values_with_mapping(self):
+        with gpiod.request_lines(
+            self.sim.dev_path,
+            config={(0, 1, 2, 3): gpiod.LineSettings(direction=Direction.OUTPUT)},
+            output_values={"baz": Value.ACTIVE, "foo": Value.ACTIVE},
+        ) as request:
+            self.assertEqual(self.sim.get_value(0), SimVal.ACTIVE)
+            self.assertEqual(self.sim.get_value(1), SimVal.INACTIVE)
+            self.assertEqual(self.sim.get_value(2), SimVal.ACTIVE)
+            self.assertEqual(self.sim.get_value(3), SimVal.INACTIVE)
+
+    def test_request_with_globally_set_output_values_bad_mapping(self):
+        with self.assertRaises(FileNotFoundError):
+            with gpiod.request_lines(
+                self.sim.dev_path,
+                config={(0, 1, 2, 3): gpiod.LineSettings(direction=Direction.OUTPUT)},
+                output_values={"foobar": Value.ACTIVE},
+            ) as request:
+                pass
+
+    def test_request_with_globally_set_output_values_bad_offset(self):
+        with self.assertRaises(ValueError):
+            with gpiod.request_lines(
+                self.sim.dev_path,
+                config={(0, 1, 2, 3): gpiod.LineSettings(direction=Direction.OUTPUT)},
+                output_values={5: Value.ACTIVE},
+            ) as request:
+                pass
+
+
+class ReconfigureRequestedLines(TestCase):
+    def setUp(self):
+        self.sim = gpiosim.Chip(num_lines=8, line_names={3: "foo", 4: "bar", 6: "baz"})
+        self.chip = gpiod.Chip(self.sim.dev_path)
+        self.req = self.chip.request_lines(
+            {(0, 2, "foo", "baz"): gpiod.LineSettings(direction=Direction.OUTPUT)}
+        )
+
+    def tearDown(self):
+        self.chip.close()
+        del self.chip
+        self.req.release()
+        del self.req
+        del self.sim
+
+    def test_reconfigure_by_offsets(self):
+        info = self.chip.get_line_info(2)
+        self.assertEqual(info.direction, Direction.OUTPUT)
+        self.req.reconfigure_lines(
+            {(0, 2, 3, 6): gpiod.LineSettings(direction=Direction.INPUT)}
+        )
+        info = self.chip.get_line_info(2)
+        self.assertEqual(info.direction, Direction.INPUT)
+
+    def test_reconfigure_by_names(self):
+        info = self.chip.get_line_info(2)
+        self.assertEqual(info.direction, Direction.OUTPUT)
+        self.req.reconfigure_lines(
+            {(0, 2, "foo", "baz"): gpiod.LineSettings(direction=Direction.INPUT)}
+        )
+        info = self.chip.get_line_info(2)
+        self.assertEqual(info.direction, Direction.INPUT)
+
+
+class ReleasedLineRequestCannotBeUsed(TestCase):
+    def test_using_released_line_request(self):
+        sim = gpiosim.Chip()
+
+        with gpiod.Chip(sim.dev_path) as chip:
+            req = chip.request_lines(config={0: None})
+            req.release()
+
+            with self.assertRaises(gpiod.RequestReleasedError):
+                req.fd
+
+
+class LineRequestSurvivesParentChip(TestCase):
+    def test_line_request_survives_parent_chip(self):
+        sim = gpiosim.Chip()
+
+        chip = gpiod.Chip(sim.dev_path)
+        try:
+            req = chip.request_lines(
+                config={0: gpiod.LineSettings(direction=Direction.INPUT)}
+            )
+        except:
+            chip.close()
+            raise
+
+        chip.close()
+        self.assertEqual(req.get_values([0]), [Value.INACTIVE])
+        req.release()
+
+
+class LineRequestStringRepresentation(TestCase):
+    def setUp(self):
+        self.sim = gpiosim.Chip(num_lines=8)
+
+    def tearDown(self):
+        del self.sim
+
+    def test_str(self):
+        with gpiod.Chip(self.sim.dev_path) as chip:
+            with chip.request_lines(config={(2, 6, 4, 1): None}) as req:
+                self.assertEqual(
+                    str(req),
+                    '<LineRequest chip="{}" num_lines=4 offsets=[2, 6, 4, 1] fd={}>'.format(
+                        self.sim.name, req.fd
+                    ),
+                )
+
+    def test_str_released(self):
+        req = gpiod.request_lines(self.sim.dev_path, config={(2, 6, 4, 1): None})
+        req.release()
+        self.assertEqual(str(req), "<LineRequest RELEASED>")
diff --git a/bindings/python/tests/tests_line_settings.py b/bindings/python/tests/tests_line_settings.py
new file mode 100644 (file)
index 0000000..36dda6d
--- /dev/null
@@ -0,0 +1,79 @@
+# SPDX-License-Identifier: LGPL-2.1-or-later
+# SPDX-FileCopyrightText: 2022 Bartosz Golaszewski <brgl@bgdev.pl>
+
+import gpiod
+
+from . import gpiosim
+from datetime import timedelta
+from gpiod.line import Direction, Edge, Bias, Drive, Value, Clock
+from unittest import TestCase
+
+
+class LineSettingsConstructor(TestCase):
+    def test_default_values(self):
+        settings = gpiod.LineSettings()
+
+        self.assertEqual(settings.direction, Direction.AS_IS)
+        self.assertEqual(settings.edge_detection, Edge.NONE)
+        self.assertEqual(settings.bias, Bias.AS_IS)
+        self.assertEqual(settings.drive, Drive.PUSH_PULL)
+        self.assertFalse(settings.active_low)
+        self.assertEqual(settings.debounce_period.total_seconds(), 0.0)
+        self.assertEqual(settings.event_clock, Clock.MONOTONIC)
+        self.assertEqual(settings.output_value, Value.INACTIVE)
+
+    def test_keyword_arguments(self):
+        settings = gpiod.LineSettings(
+            direction=Direction.INPUT,
+            edge_detection=Edge.BOTH,
+            bias=Bias.PULL_UP,
+            event_clock=Clock.REALTIME,
+        )
+
+        self.assertEqual(settings.direction, Direction.INPUT)
+        self.assertEqual(settings.edge_detection, Edge.BOTH)
+        self.assertEqual(settings.bias, Bias.PULL_UP)
+        self.assertEqual(settings.drive, Drive.PUSH_PULL)
+        self.assertFalse(settings.active_low)
+        self.assertEqual(settings.debounce_period.total_seconds(), 0.0)
+        self.assertEqual(settings.event_clock, Clock.REALTIME)
+        self.assertEqual(settings.output_value, Value.INACTIVE)
+
+
+class LineSettingsAttributes(TestCase):
+    def test_line_settings_attributes_are_mutable(self):
+        settings = gpiod.LineSettings()
+
+        settings.direction = Direction.INPUT
+        settings.edge_detection = Edge.BOTH
+        settings.bias = Bias.DISABLED
+        settings.debounce_period = timedelta(microseconds=3000)
+        settings.event_clock = Clock.HTE
+
+        self.assertEqual(settings.direction, Direction.INPUT)
+        self.assertEqual(settings.edge_detection, Edge.BOTH)
+        self.assertEqual(settings.bias, Bias.DISABLED)
+        self.assertEqual(settings.drive, Drive.PUSH_PULL)
+        self.assertFalse(settings.active_low)
+        self.assertEqual(settings.debounce_period.total_seconds(), 0.003)
+        self.assertEqual(settings.event_clock, Clock.HTE)
+        self.assertEqual(settings.output_value, Value.INACTIVE)
+
+
+class LineSettingsStringRepresentation(TestCase):
+    def setUp(self):
+        self.settings = gpiod.LineSettings(
+            direction=Direction.OUTPUT, drive=Drive.OPEN_SOURCE, active_low=True
+        )
+
+    def test_repr(self):
+        self.assertEqual(
+            repr(self.settings),
+            "LineSettings(direction=Direction.OUTPUT, edge_detection=Edge.NONE bias=Bias.AS_IS drive=Drive.OPEN_SOURCE active_low=True debounce_period=datetime.timedelta(0) event_clock=Clock.MONOTONIC output_value=Value.INACTIVE)",
+        )
+
+    def test_str(self):
+        self.assertEqual(
+            str(self.settings),
+            "<LineSettings direction=Direction.OUTPUT edge_detection=Edge.NONE bias=Bias.AS_IS drive=Drive.OPEN_SOURCE active_low=True debounce_period=0:00:00 event_clock=Clock.MONOTONIC output_value=Value.INACTIVE>",
+        )
diff --git a/bindings/python/tests/tests_module.py b/bindings/python/tests/tests_module.py
new file mode 100644 (file)
index 0000000..c6f07a6
--- /dev/null
@@ -0,0 +1,60 @@
+# SPDX-License-Identifier: GPL-2.0-or-later
+# SPDX-FileCopyrightText: 2022 Bartosz Golaszewski <brgl@bgdev.pl>
+
+import gpiod
+import os
+import unittest
+
+from . import gpiosim
+from .helpers import LinkGuard
+from unittest import TestCase
+
+
+class IsGPIOChip(TestCase):
+    def test_is_gpiochip_bad(self):
+        self.assertFalse(gpiod.is_gpiochip_device("/dev/null"))
+        self.assertFalse(gpiod.is_gpiochip_device("/dev/nonexistent"))
+
+    def test_is_gpiochip_invalid_argument(self):
+        with self.assertRaises(TypeError):
+            gpiod.is_gpiochip_device(4)
+
+    def test_is_gpiochip_superfluous_argument(self):
+        with self.assertRaises(TypeError):
+            gpiod.is_gpiochip_device("/dev/null", 4)
+
+    def test_is_gpiochip_missing_argument(self):
+        with self.assertRaises(TypeError):
+            gpiod.is_gpiochip_device()
+
+    def test_is_gpiochip_good(self):
+        sim = gpiosim.Chip()
+        self.assertTrue(gpiod.is_gpiochip_device(sim.dev_path))
+
+    def test_is_gpiochip_good_keyword_argument(self):
+        sim = gpiosim.Chip()
+        self.assertTrue(gpiod.is_gpiochip_device(path=sim.dev_path))
+
+    def test_is_gpiochip_link_good(self):
+        link = "/tmp/gpiod-py-test-link.{}".format(os.getpid())
+        sim = gpiosim.Chip()
+
+        with LinkGuard(sim.dev_path, link):
+            self.assertTrue(gpiod.is_gpiochip_device(link))
+
+    def test_is_gpiochip_link_bad(self):
+        link = "/tmp/gpiod-py-test-link.{}".format(os.getpid())
+
+        with LinkGuard("/dev/null", link):
+            self.assertFalse(gpiod.is_gpiochip_device(link))
+
+
+class VersionString(TestCase):
+
+    VERSION_PATTERN = "^\\d+\\.\\d+(\\.\\d+|\\-devel|\\-rc\\d+)?$"
+
+    def test_api_version_string(self):
+        self.assertRegex(gpiod.api_version, VersionString.VERSION_PATTERN)
+
+    def test_module_version(self):
+        self.assertRegex(gpiod.__version__, VersionString.VERSION_PATTERN)
diff --git a/bindings/rust/.gitignore b/bindings/rust/.gitignore
new file mode 100644 (file)
index 0000000..a8bda09
--- /dev/null
@@ -0,0 +1,6 @@
+# SPDX-License-Identifier: CC0-1.0
+# SPDX-FileCopyrightText: 2022 Linaro Ltd.
+# SPDX-FileCopyrightText: 2022 Viresh Kumar <viresh.kumar@linaro.org>
+
+target
+Cargo.lock
diff --git a/bindings/rust/Cargo.toml b/bindings/rust/Cargo.toml
new file mode 100644 (file)
index 0000000..e385027
--- /dev/null
@@ -0,0 +1,13 @@
+# SPDX-License-Identifier: CC0-1.0
+# SPDX-FileCopyrightText: 2022 Linaro Ltd.
+# SPDX-FileCopyrightText: 2022 Viresh Kumar <viresh.kumar@linaro.org>
+
+[workspace]
+
+members = [
+    "gpiosim-sys",
+    "libgpiod",
+    "libgpiod-sys"
+]
+
+resolver = "2"
diff --git a/bindings/rust/Makefile.am b/bindings/rust/Makefile.am
new file mode 100644 (file)
index 0000000..e89c393
--- /dev/null
@@ -0,0 +1,6 @@
+# SPDX-License-Identifier: GPL-2.0-or-later
+# SPDX-FileCopyrightText: 2022 Linaro Ltd.
+# SPDX-FileCopyrightText: 2022 Viresh Kumar <viresh.kumar@linaro.org>
+
+EXTRA_DIST = Cargo.toml
+SUBDIRS = gpiosim-sys libgpiod libgpiod-sys
diff --git a/bindings/rust/gpiosim-sys/Cargo.toml b/bindings/rust/gpiosim-sys/Cargo.toml
new file mode 100644 (file)
index 0000000..1f44a31
--- /dev/null
@@ -0,0 +1,23 @@
+# SPDX-License-Identifier: CC0-1.0
+# SPDX-FileCopyrightText: 2022 Linaro Ltd.
+# SPDX-FileCopyrightText: 2022 Viresh Kumar <viresh.kumar@linaro.org>
+
+[package]
+name = "gpiosim-sys"
+version = "0.1.0"
+authors = ["Viresh Kumar <viresh.kumar@linaro.org>"]
+description = "gpiosim header bindings"
+repository = "https://git.kernel.org/pub/scm/libs/libgpiod/libgpiod.git"
+categories = ["external-ffi-bindings", "os::linux-apis"]
+rust-version = "1.60"
+keywords = ["libgpiod", "gpio", "gpiosim"]
+license = "Apache-2.0 OR BSD-3-Clause"
+edition = "2021"
+
+[dependencies]
+errno = "0.2.8"
+libgpiod = { path = "../libgpiod" }
+
+[build-dependencies]
+bindgen = "0.63"
+cc = "1.0.46"
diff --git a/bindings/rust/gpiosim-sys/Makefile.am b/bindings/rust/gpiosim-sys/Makefile.am
new file mode 100644 (file)
index 0000000..3107223
--- /dev/null
@@ -0,0 +1,6 @@
+# SPDX-License-Identifier: GPL-2.0-or-later
+# SPDX-FileCopyrightText: 2022 Linaro Ltd.
+# SPDX-FileCopyrightText: 2022 Bartosz Golaszewski <bartosz.golaszewski@linaro.org>
+
+EXTRA_DIST = build.rs Cargo.toml README.md
+SUBDIRS = src
diff --git a/bindings/rust/gpiosim-sys/README.md b/bindings/rust/gpiosim-sys/README.md
new file mode 100644 (file)
index 0000000..b13f09a
--- /dev/null
@@ -0,0 +1,16 @@
+<!--
+SPDX-License-Identifier: CC0-1.0
+SPDX-FileCopyrightText: 2022 Linaro Ltd.
+SPDX-FileCopyrightText: 2022 Viresh Kumar <viresh.kumar@linaro.org>
+-->
+
+# Generated gpiosim Rust FFI bindings
+Automatically generated Rust FFI bindings via
+       [bindgen](https://github.com/rust-lang/rust-bindgen).
+
+## License
+
+This project is licensed under either of
+
+- [Apache License](http://www.apache.org/licenses/LICENSE-2.0), Version 2.0
+- [BSD-3-Clause License](https://opensource.org/licenses/BSD-3-Clause)
diff --git a/bindings/rust/gpiosim-sys/build.rs b/bindings/rust/gpiosim-sys/build.rs
new file mode 100644 (file)
index 0000000..c31fccb
--- /dev/null
@@ -0,0 +1,43 @@
+// SPDX-License-Identifier: Apache-2.0 OR BSD-3-Clause
+// SPDX-FileCopyrightText: 2022 Linaro Ltd.
+// SPDX-FileCopyrightText: 2022 Viresh Kumar <viresh.kumar@linaro.org>
+
+extern crate bindgen;
+
+use std::env;
+use std::path::PathBuf;
+
+fn generate_bindings() {
+    // Tell cargo to invalidate the built crate whenever following files change
+    println!("cargo:rerun-if-changed=../../../tests/gpiosim/gpiosim.h");
+
+    // The bindgen::Builder is the main entry point
+    // to bindgen, and lets you build up options for
+    // the resulting bindings.
+    let bindings = bindgen::Builder::default()
+        // The input header we would like to generate
+        // bindings for.
+        .header("../../../tests/gpiosim/gpiosim.h")
+        // Tell cargo to invalidate the built crate whenever any of the
+        // included header files changed.
+        .parse_callbacks(Box::new(bindgen::CargoCallbacks))
+        // Finish the builder and generate the bindings.
+        .generate()
+        // Unwrap the Result and panic on failure.
+        .expect("Unable to generate bindings");
+
+    // Write the bindings to the $OUT_DIR/bindings.rs file.
+    let out_path = PathBuf::from(env::var("OUT_DIR").unwrap());
+    bindings
+        .write_to_file(out_path.join("bindings.rs"))
+        .expect("Couldn't write bindings!");
+}
+
+fn main() {
+    generate_bindings();
+
+    println!("cargo:rustc-link-lib=kmod");
+    println!("cargo:rustc-link-lib=mount");
+    println!("cargo:rustc-link-search=./../../tests/gpiosim/.libs/");
+    println!("cargo:rustc-link-lib=static=gpiosim");
+}
diff --git a/bindings/rust/gpiosim-sys/src/Makefile.am b/bindings/rust/gpiosim-sys/src/Makefile.am
new file mode 100644 (file)
index 0000000..e88f477
--- /dev/null
@@ -0,0 +1,5 @@
+# SPDX-License-Identifier: GPL-2.0-or-later
+# SPDX-FileCopyrightText: 2022 Linaro Ltd.
+# SPDX-FileCopyrightText: 2022 Bartosz Golaszewski <bartosz.golaszewski@linaro.org>
+
+EXTRA_DIST = lib.rs sim.rs
diff --git a/bindings/rust/gpiosim-sys/src/lib.rs b/bindings/rust/gpiosim-sys/src/lib.rs
new file mode 100644 (file)
index 0000000..bf9ae32
--- /dev/null
@@ -0,0 +1,91 @@
+// SPDX-License-Identifier: Apache-2.0 OR BSD-3-Clause
+// SPDX-FileCopyrightText: 2022 Linaro Ltd.
+// SPDX-FileCopyrightText: 2022 Viresh Kumar <viresh.kumar@linaro.org>
+
+use libgpiod::{Error, OperationType, Result};
+
+#[allow(non_camel_case_types, non_upper_case_globals)]
+#[cfg_attr(test, allow(deref_nullptr, non_snake_case))]
+#[allow(dead_code)]
+mod bindings_raw {
+    include!(concat!(env!("OUT_DIR"), "/bindings.rs"));
+}
+use bindings_raw::*;
+
+mod sim;
+pub use sim::*;
+
+use crate::{
+    gpiosim_direction_GPIOSIM_DIRECTION_INPUT as GPIOSIM_DIRECTION_INPUT,
+    gpiosim_direction_GPIOSIM_DIRECTION_OUTPUT_HIGH as GPIOSIM_DIRECTION_OUTPUT_HIGH,
+    gpiosim_direction_GPIOSIM_DIRECTION_OUTPUT_LOW as GPIOSIM_DIRECTION_OUTPUT_LOW,
+    gpiosim_pull_GPIOSIM_PULL_DOWN as GPIOSIM_PULL_DOWN,
+    gpiosim_pull_GPIOSIM_PULL_UP as GPIOSIM_PULL_UP,
+    gpiosim_value_GPIOSIM_VALUE_ACTIVE as GPIOSIM_VALUE_ACTIVE,
+    gpiosim_value_GPIOSIM_VALUE_ERROR as GPIOSIM_VALUE_ERROR,
+    gpiosim_value_GPIOSIM_VALUE_INACTIVE as GPIOSIM_VALUE_INACTIVE,
+};
+
+/// Value settings.
+#[derive(Copy, Clone, Debug, Eq, PartialEq)]
+pub enum Value {
+    /// Active
+    Active,
+    /// Inactive
+    InActive,
+}
+
+impl Value {
+    pub(crate) fn new(val: gpiosim_value) -> Result<Self> {
+        Ok(match val {
+            GPIOSIM_VALUE_INACTIVE => Value::InActive,
+            GPIOSIM_VALUE_ACTIVE => Value::Active,
+            GPIOSIM_VALUE_ERROR => {
+                return Err(Error::OperationFailed(
+                    OperationType::SimBankGetVal,
+                    errno::errno(),
+                ))
+            }
+            _ => return Err(Error::InvalidEnumValue("Value", val)),
+        })
+    }
+}
+
+/// Direction settings.
+#[derive(Copy, Clone, Debug, Eq, PartialEq)]
+pub enum Direction {
+    /// Direction is input - for reading the value of an externally driven GPIO line.
+    Input,
+    /// Direction is output - for driving the GPIO line, value is high.
+    OutputHigh,
+    /// Direction is output - for driving the GPIO line, value is low.
+    OutputLow,
+}
+
+impl Direction {
+    fn val(self) -> gpiosim_direction {
+        match self {
+            Direction::Input => GPIOSIM_DIRECTION_INPUT,
+            Direction::OutputHigh => GPIOSIM_DIRECTION_OUTPUT_HIGH,
+            Direction::OutputLow => GPIOSIM_DIRECTION_OUTPUT_LOW,
+        }
+    }
+}
+
+/// Internal pull settings.
+#[derive(Copy, Clone, Debug, Eq, PartialEq)]
+pub enum Pull {
+    /// The internal pull-up is enabled.
+    Up,
+    /// The internal pull-down is enabled.
+    Down,
+}
+
+impl Pull {
+    fn val(self) -> gpiosim_pull {
+        match self {
+            Pull::Up => GPIOSIM_PULL_UP,
+            Pull::Down => GPIOSIM_PULL_DOWN,
+        }
+    }
+}
diff --git a/bindings/rust/gpiosim-sys/src/sim.rs b/bindings/rust/gpiosim-sys/src/sim.rs
new file mode 100644 (file)
index 0000000..71b9453
--- /dev/null
@@ -0,0 +1,330 @@
+// SPDX-License-Identifier: Apache-2.0 OR BSD-3-Clause
+// SPDX-FileCopyrightText: 2022 Linaro Ltd.
+// SPDX-FileCopyrightText: 2022 Viresh Kumar <viresh.kumar@linaro.org>
+
+use std::ffi::{CStr, CString};
+use std::os::raw::c_char;
+use std::path::PathBuf;
+use std::str;
+
+use libgpiod::{line::Offset, Error, OperationType, Result};
+
+use crate::*;
+
+/// Sim Ctx
+#[derive(Debug)]
+struct SimCtx {
+    ctx: *mut gpiosim_ctx,
+}
+
+// Safe as the pointer is guaranteed to be valid and the associated resource
+// won't be freed until the object is dropped.
+unsafe impl Send for SimCtx {}
+
+impl SimCtx {
+    fn new() -> Result<Self> {
+        // SAFETY: `gpiosim_ctx` returned by gpiosim is guaranteed to live
+        // as long as the `struct SimCtx`.
+        let ctx = unsafe { gpiosim_ctx_new() };
+        if ctx.is_null() {
+            return Err(Error::OperationFailed(
+                OperationType::SimCtxNew,
+                errno::errno(),
+            ));
+        }
+
+        Ok(Self { ctx })
+    }
+}
+
+impl Drop for SimCtx {
+    fn drop(&mut self) {
+        // SAFETY: `gpiosim_ctx` is guaranteed to be valid here.
+        unsafe { gpiosim_ctx_unref(self.ctx) }
+    }
+}
+
+/// Sim Dev
+#[derive(Debug)]
+struct SimDev {
+    dev: *mut gpiosim_dev,
+}
+
+// Safe as the pointer is guaranteed to be valid and the associated resource
+// won't be freed until the object is dropped.
+unsafe impl Send for SimDev {}
+
+impl SimDev {
+    fn new(ctx: &SimCtx) -> Result<Self> {
+        // SAFETY: `gpiosim_dev` returned by gpiosim is guaranteed to live
+        // as long as the `struct SimDev`.
+        let dev = unsafe { gpiosim_dev_new(ctx.ctx) };
+        if dev.is_null() {
+            return Err(Error::OperationFailed(
+                OperationType::SimDevNew,
+                errno::errno(),
+            ));
+        }
+
+        Ok(Self { dev })
+    }
+
+    fn enable(&self) -> Result<()> {
+        // SAFETY: `gpiosim_dev` is guaranteed to be valid here.
+        let ret = unsafe { gpiosim_dev_enable(self.dev) };
+
+        if ret == -1 {
+            Err(Error::OperationFailed(
+                OperationType::SimDevEnable,
+                errno::errno(),
+            ))
+        } else {
+            Ok(())
+        }
+    }
+
+    fn disable(&self) -> Result<()> {
+        // SAFETY: `gpiosim_dev` is guaranteed to be valid here.
+        let ret = unsafe { gpiosim_dev_disable(self.dev) };
+
+        if ret == -1 {
+            Err(Error::OperationFailed(
+                OperationType::SimDevDisable,
+                errno::errno(),
+            ))
+        } else {
+            Ok(())
+        }
+    }
+}
+
+impl Drop for SimDev {
+    fn drop(&mut self) {
+        // SAFETY: `gpiosim_dev` is guaranteed to be valid here.
+        unsafe { gpiosim_dev_unref(self.dev) }
+    }
+}
+
+/// Sim Bank
+#[derive(Debug)]
+struct SimBank {
+    bank: *mut gpiosim_bank,
+}
+
+// Safe as the pointer is guaranteed to be valid and the associated resource
+// won't be freed until the object is dropped.
+unsafe impl Send for SimBank {}
+
+impl SimBank {
+    fn new(dev: &SimDev) -> Result<Self> {
+        // SAFETY: `gpiosim_bank` returned by gpiosim is guaranteed to live
+        // as long as the `struct SimBank`.
+        let bank = unsafe { gpiosim_bank_new(dev.dev) };
+        if bank.is_null() {
+            return Err(Error::OperationFailed(
+                OperationType::SimBankNew,
+                errno::errno(),
+            ));
+        }
+
+        Ok(Self { bank })
+    }
+
+    fn chip_name(&self) -> Result<&str> {
+        // SAFETY: The string returned by gpiosim is guaranteed to live as long
+        // as the `struct SimBank`.
+        let name = unsafe { gpiosim_bank_get_chip_name(self.bank) };
+
+        // SAFETY: The string is guaranteed to be valid here.
+        unsafe { CStr::from_ptr(name) }
+            .to_str()
+            .map_err(Error::StringNotUtf8)
+    }
+
+    fn dev_path(&self) -> Result<PathBuf> {
+        // SAFETY: The string returned by gpiosim is guaranteed to live as long
+        // as the `struct SimBank`.
+        let path = unsafe { gpiosim_bank_get_dev_path(self.bank) };
+
+        // SAFETY: The string is guaranteed to be valid here.
+        let path = unsafe { CStr::from_ptr(path) }
+            .to_str()
+            .map_err(Error::StringNotUtf8)?;
+
+        Ok(PathBuf::from(path))
+    }
+
+    fn val(&self, offset: Offset) -> Result<Value> {
+        // SAFETY: `gpiosim_bank` is guaranteed to be valid here.
+        let ret = unsafe { gpiosim_bank_get_value(self.bank, offset) };
+
+        if ret == -1 {
+            Err(Error::OperationFailed(
+                OperationType::SimBankGetVal,
+                errno::errno(),
+            ))
+        } else {
+            Value::new(ret)
+        }
+    }
+
+    fn set_label(&self, label: &str) -> Result<()> {
+        let label = CString::new(label).map_err(|_| Error::InvalidString)?;
+
+        // SAFETY: `gpiosim_bank` is guaranteed to be valid here.
+        let ret = unsafe { gpiosim_bank_set_label(self.bank, label.as_ptr() as *const c_char) };
+
+        if ret == -1 {
+            Err(Error::OperationFailed(
+                OperationType::SimBankSetLabel,
+                errno::errno(),
+            ))
+        } else {
+            Ok(())
+        }
+    }
+
+    fn set_num_lines(&self, num: usize) -> Result<()> {
+        // SAFETY: `gpiosim_bank` is guaranteed to be valid here.
+        let ret = unsafe { gpiosim_bank_set_num_lines(self.bank, num) };
+        if ret == -1 {
+            Err(Error::OperationFailed(
+                OperationType::SimBankSetNumLines,
+                errno::errno(),
+            ))
+        } else {
+            Ok(())
+        }
+    }
+
+    fn set_line_name(&self, offset: Offset, name: &str) -> Result<()> {
+        let name = CString::new(name).map_err(|_| Error::InvalidString)?;
+
+        // SAFETY: `gpiosim_bank` is guaranteed to be valid here.
+        let ret = unsafe {
+            gpiosim_bank_set_line_name(self.bank, offset, name.as_ptr() as *const c_char)
+        };
+
+        if ret == -1 {
+            Err(Error::OperationFailed(
+                OperationType::SimBankSetLineName,
+                errno::errno(),
+            ))
+        } else {
+            Ok(())
+        }
+    }
+
+    fn set_pull(&self, offset: Offset, pull: Pull) -> Result<()> {
+        // SAFETY: `gpiosim_bank` is guaranteed to be valid here.
+        let ret = unsafe { gpiosim_bank_set_pull(self.bank, offset, pull.val()) };
+
+        if ret == -1 {
+            Err(Error::OperationFailed(
+                OperationType::SimBankSetPull,
+                errno::errno(),
+            ))
+        } else {
+            Ok(())
+        }
+    }
+
+    fn hog_line(&self, offset: Offset, name: &str, dir: Direction) -> Result<()> {
+        let name = CString::new(name).map_err(|_| Error::InvalidString)?;
+
+        // SAFETY: `gpiosim_bank` is guaranteed to be valid here.
+        let ret = unsafe {
+            gpiosim_bank_hog_line(self.bank, offset, name.as_ptr() as *const c_char, dir.val())
+        };
+
+        if ret == -1 {
+            Err(Error::OperationFailed(
+                OperationType::SimBankHogLine,
+                errno::errno(),
+            ))
+        } else {
+            Ok(())
+        }
+    }
+}
+
+impl Drop for SimBank {
+    fn drop(&mut self) {
+        // SAFETY: `gpiosim_bank` is guaranteed to be valid here.
+        unsafe { gpiosim_bank_unref(self.bank) }
+    }
+}
+
+/// GPIO SIM
+#[derive(Debug)]
+pub struct Sim {
+    _ctx: SimCtx,
+    dev: SimDev,
+    bank: SimBank,
+}
+
+impl Sim {
+    pub fn new(ngpio: Option<usize>, label: Option<&str>, enable: bool) -> Result<Self> {
+        let ctx = SimCtx::new()?;
+        let dev = SimDev::new(&ctx)?;
+        let bank = SimBank::new(&dev)?;
+
+        if let Some(ngpio) = ngpio {
+            bank.set_num_lines(ngpio)?;
+        }
+
+        if let Some(label) = label {
+            bank.set_label(label)?;
+        }
+
+        if enable {
+            dev.enable()?;
+        }
+
+        Ok(Self {
+            _ctx: ctx,
+            dev,
+            bank,
+        })
+    }
+
+    pub fn chip_name(&self) -> &str {
+        self.bank.chip_name().unwrap()
+    }
+
+    pub fn dev_path(&self) -> PathBuf {
+        self.bank.dev_path().unwrap()
+    }
+
+    pub fn val(&self, offset: Offset) -> Result<Value> {
+        self.bank.val(offset)
+    }
+
+    pub fn set_label(&self, label: &str) -> Result<()> {
+        self.bank.set_label(label)
+    }
+
+    pub fn set_num_lines(&self, num: usize) -> Result<()> {
+        self.bank.set_num_lines(num)
+    }
+
+    pub fn set_line_name(&self, offset: Offset, name: &str) -> Result<()> {
+        self.bank.set_line_name(offset, name)
+    }
+
+    pub fn set_pull(&self, offset: Offset, pull: Pull) -> Result<()> {
+        self.bank.set_pull(offset, pull)
+    }
+
+    pub fn hog_line(&self, offset: Offset, name: &str, dir: Direction) -> Result<()> {
+        self.bank.hog_line(offset, name, dir)
+    }
+
+    pub fn enable(&self) -> Result<()> {
+        self.dev.enable()
+    }
+
+    pub fn disable(&self) -> Result<()> {
+        self.dev.disable()
+    }
+}
diff --git a/bindings/rust/libgpiod-sys/Cargo.toml b/bindings/rust/libgpiod-sys/Cargo.toml
new file mode 100644 (file)
index 0000000..b4d26e9
--- /dev/null
@@ -0,0 +1,28 @@
+# SPDX-License-Identifier: CC0-1.0
+# SPDX-FileCopyrightText: 2022 Linaro Ltd.
+# SPDX-FileCopyrightText: 2022 Viresh Kumar <viresh.kumar@linaro.org>
+
+[package]
+name = "libgpiod-sys"
+version = "0.1.0"
+authors = ["Viresh Kumar <viresh.kumar@linaro.org>"]
+description = "libgpiod public header bindings"
+repository = "https://git.kernel.org/pub/scm/libs/libgpiod/libgpiod.git"
+categories = ["external-ffi-bindings", "os::linux-apis"]
+rust-version = "1.60"
+keywords = ["libgpiod", "gpio"]
+license = "Apache-2.0 OR BSD-3-Clause"
+edition = "2021"
+
+exclude = [
+    "Makefile.am",
+]
+
+[dependencies]
+
+[build-dependencies]
+bindgen = "0.63"
+system-deps = "2.0"
+
+[package.metadata.system-deps]
+libgpiod = "2"
diff --git a/bindings/rust/libgpiod-sys/Makefile.am b/bindings/rust/libgpiod-sys/Makefile.am
new file mode 100644 (file)
index 0000000..667f3de
--- /dev/null
@@ -0,0 +1,6 @@
+# SPDX-License-Identifier: GPL-2.0-or-later
+# SPDX-FileCopyrightText: 2022 Linaro Ltd.
+# SPDX-FileCopyrightText: 2022 Bartosz Golaszewski <bartosz.golaszewski@linaro.org>
+
+EXTRA_DIST = build.rs Cargo.toml README.md wrapper.h
+SUBDIRS = src
diff --git a/bindings/rust/libgpiod-sys/README.md b/bindings/rust/libgpiod-sys/README.md
new file mode 100644 (file)
index 0000000..05acd9e
--- /dev/null
@@ -0,0 +1,36 @@
+<!--
+SPDX-License-Identifier: CC0-1.0
+SPDX-FileCopyrightText: 2022 Linaro Ltd.
+SPDX-FileCopyrightText: 2022 Viresh Kumar <viresh.kumar@linaro.org>
+-->
+
+# Generated libgpiod-sys Rust FFI bindings
+Automatically generated Rust FFI bindings via
+       [bindgen](https://github.com/rust-lang/rust-bindgen).
+
+Typically, you will want to use the safe `libgpiod` wrapper crate instead of
+these unsafe wrappers around the C lib.
+
+## Build requirements
+
+A compatible variant of the C library needs to detectable using pkg-config.
+Alternatively, one can inform the build system about the location of the
+libs and headers by setting environment variables. The mechanism for that is
+documented in the
+[system_deps crate documentation](https://docs.rs/system-deps/6.1.0/system_deps/#overriding-build-flags).
+
+If installing libgpiod is undesired, one can set the following environent
+variables in order to build against the intermediate build results of a `make`
+build of the C lib (paths are relative to the Cargo.toml):
+
+       export SYSTEM_DEPS_LIBGPIOD_NO_PKG_CONFIG=1
+       export SYSTEM_DEPS_LIBGPIOD_SEARCH_NATIVE="<PATH-TO-LIBGPIOD>/lib/.libs/"
+       export SYSTEM_DEPS_LIBGPIOD_LIB=gpiod
+       export SYSTEM_DEPS_LIBGPIOD_INCLUDE="<PATH-TO-LIBGPIOD>/include/"
+
+## License
+
+This project is licensed under either of
+
+- [Apache License](http://www.apache.org/licenses/LICENSE-2.0), Version 2.0
+- [BSD-3-Clause License](https://opensource.org/licenses/BSD-3-Clause)
diff --git a/bindings/rust/libgpiod-sys/build.rs b/bindings/rust/libgpiod-sys/build.rs
new file mode 100644 (file)
index 0000000..9e6a93c
--- /dev/null
@@ -0,0 +1,51 @@
+// SPDX-License-Identifier: Apache-2.0 OR BSD-3-Clause
+// SPDX-FileCopyrightText: 2022-2023 Linaro Ltd.
+// SPDX-FileCopyrightText: 2022 Viresh Kumar <viresh.kumar@linaro.org>
+// SPDX-FileCopyrightText: 2023 Erik Schilling <erik.schilling@linaro.org>
+
+use std::env;
+use std::path::PathBuf;
+
+fn main() {
+    // Probe dependency info based on the metadata from Cargo.toml
+    // (and potentially other sources like environment, pkg-config, ...)
+    // https://docs.rs/system-deps/latest/system_deps/#overriding-build-flags
+    let libs = system_deps::Config::new().probe().unwrap();
+
+    // Tell cargo to invalidate the built crate whenever following files change
+    println!("cargo:rerun-if-changed=wrapper.h");
+
+    // The bindgen::Builder is the main entry point
+    // to bindgen, and lets you build up options for
+    // the resulting bindings.
+    let mut builder = bindgen::Builder::default()
+        // The input header we would like to generate
+        // bindings for.
+        .header("wrapper.h")
+        // Tell cargo to invalidate the built crate whenever any of the
+        // included header files changed.
+        .parse_callbacks(Box::new(bindgen::CargoCallbacks));
+
+    // Inform bindgen about the include paths identified by system_deps.
+    for (_name, lib) in libs {
+        for include_path in lib.include_paths {
+            builder = builder.clang_arg("-I").clang_arg(
+                include_path
+                    .to_str()
+                    .expect("Failed to convert include_path to &str!"),
+            );
+        }
+    }
+
+    // Finish the builder and generate the bindings.
+    let bindings = builder
+        .generate()
+        // Unwrap the Result and panic on failure.
+        .expect("Unable to generate bindings");
+
+    // Write the bindings to the $OUT_DIR/bindings.rs file.
+    let out_path = PathBuf::from(env::var("OUT_DIR").unwrap());
+    bindings
+        .write_to_file(out_path.join("bindings.rs"))
+        .expect("Couldn't write bindings!");
+}
diff --git a/bindings/rust/libgpiod-sys/src/Makefile.am b/bindings/rust/libgpiod-sys/src/Makefile.am
new file mode 100644 (file)
index 0000000..0ef728b
--- /dev/null
@@ -0,0 +1,5 @@
+# SPDX-License-Identifier: GPL-2.0-or-later
+# SPDX-FileCopyrightText: 2022 Linaro Ltd.
+# SPDX-FileCopyrightText: 2022 Bartosz Golaszewski <bartosz.golaszewski@linaro.org>
+
+EXTRA_DIST = lib.rs
diff --git a/bindings/rust/libgpiod-sys/src/lib.rs b/bindings/rust/libgpiod-sys/src/lib.rs
new file mode 100644 (file)
index 0000000..06f1a50
--- /dev/null
@@ -0,0 +1,11 @@
+// SPDX-License-Identifier: Apache-2.0 OR BSD-3-Clause
+// SPDX-FileCopyrightText: 2022 Linaro Ltd.
+// SPDX-FileCopyrightText: 2022 Viresh Kumar <viresh.kumar@linaro.org>
+
+#[allow(non_camel_case_types, non_upper_case_globals)]
+#[cfg_attr(test, allow(deref_nullptr, non_snake_case))]
+
+mod bindings_raw {
+    include!(concat!(env!("OUT_DIR"), "/bindings.rs"));
+}
+pub use bindings_raw::*;
diff --git a/bindings/rust/libgpiod-sys/wrapper.h b/bindings/rust/libgpiod-sys/wrapper.h
new file mode 100644 (file)
index 0000000..7a350a4
--- /dev/null
@@ -0,0 +1,5 @@
+/* SPDX-License-Identifier: Apache-2.0 OR BSD-3-Clause */
+/* SPDX-FileCopyrightText: 2023 Linaro Ltd. */
+/* SPDX-FileCopyrightText: 2023 Erik Schilling <erik.schilling@linaro.org> */
+
+#include <gpiod.h>
diff --git a/bindings/rust/libgpiod/Cargo.toml b/bindings/rust/libgpiod/Cargo.toml
new file mode 100644 (file)
index 0000000..3ce05df
--- /dev/null
@@ -0,0 +1,32 @@
+# SPDX-License-Identifier: CC0-1.0
+# SPDX-FileCopyrightText: 2022 Linaro Ltd.
+# SPDX-FileCopyrightText: 2022 Viresh Kumar <viresh.kumar@linaro.org>
+
+[package]
+name = "libgpiod"
+version = "0.2.1"
+authors = ["Viresh Kumar <viresh.kumar@linaro.org>"]
+description = "libgpiod wrappers"
+repository = "https://git.kernel.org/pub/scm/libs/libgpiod/libgpiod.git"
+categories = ["api-bindings", "hardware-support", "embedded", "os::linux-apis"]
+rust-version = "1.60"
+keywords = ["libgpiod", "gpio"]
+license = "Apache-2.0 OR BSD-3-Clause"
+edition = "2021"
+
+exclude = [
+    "Makefile.am",
+]
+
+[features]
+vnext = []
+
+[dependencies]
+errno = "0.2.8"
+intmap = "2.0.0"
+libc = "0.2.39"
+libgpiod-sys = { version = "0.1", path = "../libgpiod-sys" }
+thiserror = "1.0"
+
+[dev-dependencies]
+gpiosim-sys = { path = "../gpiosim-sys" }
diff --git a/bindings/rust/libgpiod/Makefile.am b/bindings/rust/libgpiod/Makefile.am
new file mode 100644 (file)
index 0000000..619e36c
--- /dev/null
@@ -0,0 +1,28 @@
+# SPDX-License-Identifier: GPL-2.0-or-later
+# SPDX-FileCopyrightText: 2022 Linaro Ltd.
+# SPDX-FileCopyrightText: 2022 Bartosz Golaszewski <bartosz.golaszewski@linaro.org>
+
+# We do not want to build against the system libs when building with make. So we
+# specify the paths to the build directory of the C lib.
+command = SYSTEM_DEPS_LIBGPIOD_NO_PKG_CONFIG=1 \
+               SYSTEM_DEPS_LIBGPIOD_SEARCH_NATIVE="${PWD}/../../../lib/.libs/" \
+               SYSTEM_DEPS_LIBGPIOD_LIB=gpiod \
+               SYSTEM_DEPS_LIBGPIOD_INCLUDE="${PWD}/../../../include/"  \
+               cargo build --features=vnext --release --lib
+
+if WITH_TESTS
+command += --tests
+endif
+
+if WITH_EXAMPLES
+command += --examples
+endif
+
+all:
+       $(command)
+
+clean:
+       cargo clean
+
+EXTRA_DIST = Cargo.toml
+SUBDIRS = examples src tests
diff --git a/bindings/rust/libgpiod/README.md b/bindings/rust/libgpiod/README.md
new file mode 100644 (file)
index 0000000..c86b06e
--- /dev/null
@@ -0,0 +1,38 @@
+<!--
+SPDX-License-Identifier: CC0-1.0
+SPDX-FileCopyrightText: 2023 Linaro Ltd.
+SPDX-FileCopyrightText: 2023 Erik Schilling <erik.schilling@linaro.org>
+-->
+
+# Safe wrapper around Rust FFI bindings for libgpiod
+
+[libgpiod](https://git.kernel.org/pub/scm/libs/libgpiod/libgpiod.git/tree/README)
+is a C library that provides an easy to use abstraction over the Linux GPIO
+character driver. This crate builds on top of `libgpiod-sys` and exports a safe
+interface to the C library.
+
+## Build requirements
+
+By default, `libgpiod-sys` builds against the libgpiod version identified via
+`pkg-config`. See the `README.md` of `libgpiod-sys` for options to override
+that.
+
+Currently at least libgpiod 2.0 is required with the default feature set.
+
+## Features
+
+The Rust bindings will usually be built against whatever libgpiod version a
+system provides. Hence, only the functionality of the oldest supported libgpiod
+C library will be exposed by default.
+
+Setting flags allows to increase the base version and export features of newer
+versions:
+
+- `vnext`: The upcoming, still unreleased version of the C lib
+
+## License
+
+This project is licensed under either of
+
+- [Apache License](http://www.apache.org/licenses/LICENSE-2.0), Version 2.0
+- [BSD-3-Clause License](https://opensource.org/licenses/BSD-3-Clause)
diff --git a/bindings/rust/libgpiod/examples/Makefile.am b/bindings/rust/libgpiod/examples/Makefile.am
new file mode 100644 (file)
index 0000000..48b182c
--- /dev/null
@@ -0,0 +1,18 @@
+# SPDX-License-Identifier: GPL-2.0-or-later
+# SPDX-FileCopyrightText: 2022 Linaro Ltd.
+# SPDX-FileCopyrightText: 2022 Bartosz Golaszewski <bartosz.golaszewski@linaro.org>
+
+EXTRA_DIST = \
+       buffered_event_lifetimes.rs \
+       find_line_by_name.rs \
+       get_chip_info.rs \
+       get_line_info.rs \
+       get_line_value.rs \
+       get_multiple_line_values.rs \
+       reconfigure_input_to_output.rs \
+       toggle_line_value.rs \
+       toggle_multiple_line_values.rs \
+       watch_line_info.rs \
+       watch_line_rising.rs \
+       watch_line_value.rs \
+       watch_multiple_line_values.rs
diff --git a/bindings/rust/libgpiod/examples/buffered_event_lifetimes.rs b/bindings/rust/libgpiod/examples/buffered_event_lifetimes.rs
new file mode 100644 (file)
index 0000000..8dbb496
--- /dev/null
@@ -0,0 +1,58 @@
+// SPDX-License-Identifier: Apache-2.0 OR BSD-3-Clause
+// SPDX-FileCopyrightText: 2022 Linaro Ltd.
+// SPDX-FileCopyrightText: 2022 Viresh Kumar <viresh.kumar@linaro.org>
+//
+// An example demonstrating that an edge event must be cloned to outlive
+// subsequent writes to the containing event buffer.
+
+use libgpiod::line;
+
+fn main() -> libgpiod::Result<()> {
+    // Example configuration - customize to suit your situation
+    let chip_path = "/dev/gpiochip0";
+    let line_offset = 5;
+
+    let mut lsettings = line::Settings::new()?;
+    lsettings.set_edge_detection(Some(line::Edge::Both))?;
+
+    let mut lconfig = line::Config::new()?;
+    lconfig.add_line_settings(&[line_offset], lsettings)?;
+
+    let mut rconfig = libgpiod::request::Config::new()?;
+    rconfig.set_consumer("buffered-event-lifetimes")?;
+
+    let chip = libgpiod::chip::Chip::open(&chip_path)?;
+    let request = chip.request_lines(Some(&rconfig), &lconfig)?;
+
+    let mut buffer = libgpiod::request::Buffer::new(4)?;
+
+    loop {
+        // Blocks until at least one event is available
+        let mut events = request.read_edge_events(&mut buffer)?;
+
+        // This can't be used across the next read_edge_events().
+        let event = events.next().unwrap()?;
+
+        // This will out live `event` and the next read_edge_events().
+        let cloned_event = libgpiod::request::Event::try_clone(event)?;
+
+        let events = request.read_edge_events(&mut buffer)?;
+        for event in events {
+            let event = event?;
+            println!(
+                "line: {}  type: {:?}  event #{}",
+                event.line_offset(),
+                event.event_type(),
+                event.line_seqno(),
+            );
+        }
+
+        // `cloned_event` is still available to be used.
+        println!(
+            "line: {}  type: {:?}  event #{}",
+            cloned_event.line_offset(),
+            cloned_event.event_type(),
+            cloned_event.line_seqno(),
+        );
+    }
+}
diff --git a/bindings/rust/libgpiod/examples/find_line_by_name.rs b/bindings/rust/libgpiod/examples/find_line_by_name.rs
new file mode 100644 (file)
index 0000000..f7b9919
--- /dev/null
@@ -0,0 +1,29 @@
+// SPDX-License-Identifier: Apache-2.0 OR BSD-3-Clause
+// SPDX-FileCopyrightText: 2023 Kent Gibson <warthog618@gmail.com>
+//
+// Minimal example of finding a line with the given name.
+
+fn main() -> libgpiod::Result<()> {
+    // Example configuration - customize to suit your situation
+    let line_name = "GPIO19";
+
+    // Names are not guaranteed unique, so this finds the first line with
+    // the given name.
+    for chip in libgpiod::gpiochip_devices(&"/dev")? {
+        let offset = chip.line_offset_from_name(line_name);
+
+        if offset.is_ok() {
+            let info = chip.info()?;
+            println!(
+                "{}: {} {}",
+                line_name,
+                info.name()?,
+                offset?
+            );
+            return Ok(());
+        }
+    }
+
+    println!("line '{}' not found", line_name);
+    Ok(())
+}
diff --git a/bindings/rust/libgpiod/examples/get_chip_info.rs b/bindings/rust/libgpiod/examples/get_chip_info.rs
new file mode 100644 (file)
index 0000000..cc23c86
--- /dev/null
@@ -0,0 +1,22 @@
+// SPDX-License-Identifier: Apache-2.0 OR BSD-3-Clause
+// SPDX-FileCopyrightText: 2023 Kent Gibson <warthog618@gmail.com>
+//
+// Minimal example of reading the info for a chip.
+
+use libgpiod::{self, Result};
+
+fn main() -> Result<()> {
+    // Example configuration - customize to suit your situation
+    let chip_path = "/dev/gpiochip0";
+
+    let chip = libgpiod::chip::Chip::open(&chip_path)?;
+    let info = chip.info()?;
+    println!(
+        "{} [{}] ({} lines)",
+        info.name()?,
+        info.label()?,
+        info.num_lines(),
+    );
+
+    Ok(())
+}
diff --git a/bindings/rust/libgpiod/examples/get_line_info.rs b/bindings/rust/libgpiod/examples/get_line_info.rs
new file mode 100644 (file)
index 0000000..086db90
--- /dev/null
@@ -0,0 +1,37 @@
+// SPDX-License-Identifier: Apache-2.0 OR BSD-3-Clause
+// SPDX-FileCopyrightText: 2023 Kent Gibson <warthog618@gmail.com>
+//
+// Minimal example of reading the info for a line.
+
+use libgpiod::line::Direction;
+
+fn main() -> libgpiod::Result<()> {
+    // Example configuration - customize to suit your situation
+    let chip_path = "/dev/gpiochip0";
+    let line_offset = 3;
+
+    let chip = libgpiod::chip::Chip::open(&chip_path)?;
+    let info = chip.line_info(line_offset)?;
+
+    let name = info.name().unwrap_or("unnamed");
+    let consumer = info.consumer().unwrap_or("unused");
+
+    let dir = match info.direction()? {
+        Direction::AsIs => "none",
+        Direction::Input => "input",
+        Direction::Output => "output",
+    };
+
+    let low = if info.is_active_low() {
+        "active-low"
+    } else {
+        "active-high"
+    };
+
+    println!(
+        "line {:>3}: {:>12} {:>12} {:>8} {:>10}",
+        line_offset, name, consumer, dir, low
+    );
+
+    Ok(())
+}
diff --git a/bindings/rust/libgpiod/examples/get_line_value.rs b/bindings/rust/libgpiod/examples/get_line_value.rs
new file mode 100644 (file)
index 0000000..39141e2
--- /dev/null
@@ -0,0 +1,28 @@
+// SPDX-License-Identifier: Apache-2.0 OR BSD-3-Clause
+// SPDX-FileCopyrightText: 2023 Kent Gibson <warthog618@gmail.com>
+//
+// Minimal example of reading a single line.
+
+use libgpiod::line;
+
+fn main() -> libgpiod::Result<()> {
+    // Example configuration - customize to suit your situation
+    let chip_path = "/dev/gpiochip0";
+    let line_offset = 5;
+
+    let mut lsettings = line::Settings::new()?;
+    lsettings.set_direction(line::Direction::Input)?;
+
+    let mut lconfig = line::Config::new()?;
+    lconfig.add_line_settings(&[line_offset], lsettings)?;
+
+    let mut rconfig = libgpiod::request::Config::new()?;
+    rconfig.set_consumer("get-line-value")?;
+
+    let chip = libgpiod::chip::Chip::open(&chip_path)?;
+    let request = chip.request_lines(Some(&rconfig), &lconfig)?;
+
+    let value = request.value(line_offset)?;
+    println!("{}={:?}", line_offset, value);
+    Ok(())
+}
diff --git a/bindings/rust/libgpiod/examples/get_multiple_line_values.rs b/bindings/rust/libgpiod/examples/get_multiple_line_values.rs
new file mode 100644 (file)
index 0000000..a1be5a6
--- /dev/null
@@ -0,0 +1,29 @@
+// SPDX-License-Identifier: Apache-2.0 OR BSD-3-Clause
+// SPDX-FileCopyrightText: 2023 Kent Gibson <warthog618@gmail.com>
+//
+// Minimal example of reading multiple lines.
+
+use libgpiod::line::{self, Direction};
+
+fn main() -> libgpiod::Result<()> {
+    // Example configuration - customize to suit your situation
+    let chip_path = "/dev/gpiochip0";
+    let line_offsets = [5, 3, 7];
+
+    let mut lsettings = line::Settings::new()?;
+    let mut lconfig = line::Config::new()?;
+
+    lsettings.set_direction(Direction::Input)?;
+    lconfig.add_line_settings(&line_offsets, lsettings)?;
+
+    let chip = libgpiod::chip::Chip::open(&chip_path)?;
+
+    let mut rconfig = libgpiod::request::Config::new()?;
+    rconfig.set_consumer("get-multiple-line-values")?;
+
+    let request = chip.request_lines(Some(&rconfig), &lconfig)?;
+    let values = request.values()?;
+
+    println!("{:?}", values);
+    Ok(())
+}
diff --git a/bindings/rust/libgpiod/examples/reconfigure_input_to_output.rs b/bindings/rust/libgpiod/examples/reconfigure_input_to_output.rs
new file mode 100644 (file)
index 0000000..fb5402b
--- /dev/null
@@ -0,0 +1,42 @@
+// SPDX-License-Identifier: Apache-2.0 OR BSD-3-Clause
+// SPDX-FileCopyrightText: 2023 Kent Gibson <warthog618@gmail.com>
+//
+// Example of a bi-directional line requested as input and then switched to output.
+
+use libgpiod::line;
+
+fn main() -> libgpiod::Result<()> {
+    // Example configuration - customize to suit your situation
+    let chip_path = "/dev/gpiochip0";
+    let line_offset = 5;
+
+    let mut lsettings = line::Settings::new()?;
+    lsettings.set_direction(line::Direction::Input)?;
+
+    let mut lconfig = line::Config::new()?;
+    lconfig.add_line_settings(&[line_offset], lsettings)?;
+
+    let mut rconfig = libgpiod::request::Config::new()?;
+    rconfig.set_consumer("reconfigure-input-to-output")?;
+
+    let chip = libgpiod::chip::Chip::open(&chip_path)?;
+    // request the line initially as an input
+    let mut request = chip.request_lines(Some(&rconfig), &lconfig)?;
+
+    // read the current line value
+    let value = request.value(line_offset)?;
+    println!("{}={:?} (input)", line_offset, value);
+
+    // switch the line to an output and drive it low
+    let mut lsettings = line::Settings::new()?;
+    lsettings.set_direction(line::Direction::Output)?;
+    lsettings.set_output_value(line::Value::InActive)?;
+    lconfig.add_line_settings(&[line_offset], lsettings)?;
+    request.reconfigure_lines(&lconfig)?;
+
+    // report the current driven value
+    let value = request.value(line_offset)?;
+    println!("{}={:?} (output)", line_offset, value);
+
+    Ok(())
+}
diff --git a/bindings/rust/libgpiod/examples/toggle_line_value.rs b/bindings/rust/libgpiod/examples/toggle_line_value.rs
new file mode 100644 (file)
index 0000000..6d5f697
--- /dev/null
@@ -0,0 +1,42 @@
+// SPDX-License-Identifier: Apache-2.0 OR BSD-3-Clause
+// SPDX-FileCopyrightText: 2023 Kent Gibson <warthog618@gmail.com>
+//
+// Minimal example of toggling a single line.
+
+use core::time::Duration;
+use libgpiod::line::{self, Value};
+
+fn toggle_value(value: Value) -> Value {
+    match value {
+        Value::Active => Value::InActive,
+        Value::InActive => Value::Active,
+    }
+}
+
+fn main() -> libgpiod::Result<()> {
+    // Example configuration - customize to suit your situation
+    let chip_path = "/dev/gpiochip0";
+    let line_offset = 5;
+    let mut value = Value::Active;
+
+    let mut settings = line::Settings::new()?;
+    settings
+        .set_direction(line::Direction::Output)?
+        .set_output_value(value)?;
+
+    let mut lconfig = line::Config::new()?;
+    lconfig.add_line_settings(&[line_offset], settings)?;
+
+    let mut rconfig = libgpiod::request::Config::new()?;
+    rconfig.set_consumer("toggle-line-value")?;
+
+    let chip = libgpiod::chip::Chip::open(&chip_path)?;
+    let mut req = chip.request_lines(Some(&rconfig), &lconfig)?;
+
+    loop {
+        println!("{}={:?}", line_offset, value);
+        std::thread::sleep(Duration::from_secs(1));
+        value = toggle_value(value);
+        req.set_value(line_offset, value)?;
+    }
+}
diff --git a/bindings/rust/libgpiod/examples/toggle_multiple_line_values.rs b/bindings/rust/libgpiod/examples/toggle_multiple_line_values.rs
new file mode 100644 (file)
index 0000000..b7e6915
--- /dev/null
@@ -0,0 +1,55 @@
+// SPDX-License-Identifier: Apache-2.0 OR BSD-3-Clause
+// SPDX-FileCopyrightText: 2023 Kent Gibson <warthog618@gmail.com>
+//
+// Minimal example of toggling multiple lines.
+
+use core::time::Duration;
+use libgpiod::line::{self, Offset, Value};
+
+fn toggle_value(value: Value) -> Value {
+    match value {
+        Value::Active => Value::InActive,
+        Value::InActive => Value::Active,
+    }
+}
+
+fn toggle_values(values: &mut [Value]) {
+    for i in values.iter_mut() {
+        *i = toggle_value(*i);
+    }
+}
+
+fn print_values(offsets: &[Offset], values: &[Value]) {
+    for i in 0..offsets.len() {
+        print!("{}={:?} ", offsets[i], values[i]);
+    }
+    println!();
+}
+
+fn main() -> libgpiod::Result<()> {
+    // Example configuration - customize to suit your situation
+    let chip_path = "/dev/gpiochip0";
+    let line_offsets = [5, 3, 7];
+    let mut values = vec![Value::Active, Value::Active, Value::InActive];
+
+    let mut lsettings = line::Settings::new()?;
+    lsettings.set_direction(line::Direction::Output)?;
+
+    let mut lconfig = line::Config::new()?;
+    lconfig
+        .add_line_settings(&line_offsets, lsettings)?
+        .set_output_values(&values)?;
+
+    let mut rconfig = libgpiod::request::Config::new()?;
+    rconfig.set_consumer("toggle-multiple-line-values")?;
+
+    let chip = libgpiod::chip::Chip::open(&chip_path)?;
+    let mut req = chip.request_lines(Some(&rconfig), &lconfig)?;
+
+    loop {
+        print_values(&line_offsets, &values);
+        std::thread::sleep(Duration::from_secs(1));
+        toggle_values(&mut values);
+        req.set_values(&values)?;
+    }
+}
diff --git a/bindings/rust/libgpiod/examples/watch_line_info.rs b/bindings/rust/libgpiod/examples/watch_line_info.rs
new file mode 100644 (file)
index 0000000..e84ce13
--- /dev/null
@@ -0,0 +1,32 @@
+// SPDX-License-Identifier: Apache-2.0 OR BSD-3-Clause
+// SPDX-FileCopyrightText: 2023 Kent Gibson <warthog618@gmail.com>
+//
+// Minimal example of watching for info changes on particular lines.
+
+use libgpiod::line::InfoChangeKind;
+
+fn main() -> libgpiod::Result<()> {
+    // Example configuration - customize to suit your situation
+    let chip_path = "/dev/gpiochip0";
+    let line_offsets = [5, 3, 7];
+
+    let chip = libgpiod::chip::Chip::open(&chip_path)?;
+    for offset in line_offsets {
+        let _info = chip.watch_line_info(offset)?;
+    }
+
+    loop {
+        // Blocks until at least one event is available.
+        let event = chip.read_info_event()?;
+        println!(
+            "line: {} {:<9} {:?}",
+            event.line_info()?.offset(),
+            match event.event_type()? {
+                InfoChangeKind::LineRequested => "Requested",
+                InfoChangeKind::LineReleased => "Released",
+                InfoChangeKind::LineConfigChanged => "Reconfig",
+            },
+            event.timestamp()
+        );
+    }
+}
diff --git a/bindings/rust/libgpiod/examples/watch_line_rising.rs b/bindings/rust/libgpiod/examples/watch_line_rising.rs
new file mode 100644 (file)
index 0000000..81a2407
--- /dev/null
@@ -0,0 +1,44 @@
+// SPDX-License-Identifier: Apache-2.0 OR BSD-3-Clause
+// SPDX-FileCopyrightText: 2023 Kent Gibson <warthog618@gmail.com>
+//
+// Minimal example of watching for edges on a single line.
+
+use libgpiod::line;
+
+fn main() -> libgpiod::Result<()> {
+    // Example configuration - customize to suit your situation
+    let chip_path = "/dev/gpiochip0";
+    let line_offset = 5;
+
+    let mut lsettings = line::Settings::new()?;
+    lsettings.set_edge_detection(Some(line::Edge::Rising))?;
+
+    let mut lconfig = line::Config::new()?;
+    lconfig.add_line_settings(&[line_offset], lsettings)?;
+
+    let mut rconfig = libgpiod::request::Config::new()?;
+    rconfig.set_consumer("watch-line-value")?;
+
+    let chip = libgpiod::chip::Chip::open(&chip_path)?;
+    let request = chip.request_lines(Some(&rconfig), &lconfig)?;
+
+    // A larger buffer is an optimisation for reading bursts of events from the
+    // kernel, but that is not necessary in this case, so 1 is fine.
+    let mut buffer = libgpiod::request::Buffer::new(1)?;
+    loop {
+        // blocks until at least one event is available
+        let events = request.read_edge_events(&mut buffer)?;
+        for event in events {
+            let event = event?;
+            println!(
+                "line: {}  type: {:<7}  event #{}",
+                event.line_offset(),
+                match event.event_type()? {
+                    line::EdgeKind::Rising => "Rising",
+                    line::EdgeKind::Falling => "Falling",
+                },
+                event.line_seqno()
+            );
+        }
+    }
+}
diff --git a/bindings/rust/libgpiod/examples/watch_line_value.rs b/bindings/rust/libgpiod/examples/watch_line_value.rs
new file mode 100644 (file)
index 0000000..3bf40af
--- /dev/null
@@ -0,0 +1,50 @@
+// SPDX-License-Identifier: Apache-2.0 OR BSD-3-Clause
+// SPDX-FileCopyrightText: 2023 Kent Gibson <warthog618@gmail.com>
+//
+// Minimal example of watching for edges on a single line.
+
+use libgpiod::line;
+use std::time::Duration;
+
+fn main() -> libgpiod::Result<()> {
+    // Example configuration - customize to suit your situation
+    let chip_path = "/dev/gpiochip0";
+    let line_offset = 5;
+
+    let mut lsettings = line::Settings::new()?;
+    // Assume a button connecting the pin to ground,
+    // so pull it up and provide some debounce.
+    lsettings
+        .set_edge_detection(Some(line::Edge::Both))?
+        .set_bias(Some(line::Bias::PullUp))?
+        .set_debounce_period(Duration::from_millis(10));
+
+    let mut lconfig = line::Config::new()?;
+    lconfig.add_line_settings(&[line_offset], lsettings)?;
+
+    let mut rconfig = libgpiod::request::Config::new()?;
+    rconfig.set_consumer("watch-line-value")?;
+
+    let chip = libgpiod::chip::Chip::open(&chip_path)?;
+    let request = chip.request_lines(Some(&rconfig), &lconfig)?;
+
+    // A larger buffer is an optimisation for reading bursts of events from the
+    // kernel, but that is not necessary in this case, so 1 is fine.
+    let mut buffer = libgpiod::request::Buffer::new(1)?;
+    loop {
+        // blocks until at least one event is available
+        let events = request.read_edge_events(&mut buffer)?;
+        for event in events {
+            let event = event?;
+            println!(
+                "line: {}  type: {:<7}  event #{}",
+                event.line_offset(),
+                match event.event_type()? {
+                    line::EdgeKind::Rising => "Rising",
+                    line::EdgeKind::Falling => "Falling",
+                },
+                event.line_seqno()
+            );
+        }
+    }
+}
diff --git a/bindings/rust/libgpiod/examples/watch_multiple_line_values.rs b/bindings/rust/libgpiod/examples/watch_multiple_line_values.rs
new file mode 100644 (file)
index 0000000..3fc88ba
--- /dev/null
@@ -0,0 +1,46 @@
+// SPDX-License-Identifier: Apache-2.0 OR BSD-3-Clause
+// SPDX-FileCopyrightText: 2023 Kent Gibson <warthog618@gmail.com>
+//
+// Minimal example of watching for edges on multiple lines.
+
+use libgpiod::{
+    line::{self, EdgeKind},
+    request,
+};
+
+fn main() -> libgpiod::Result<()> {
+    // Example configuration - customize to suit your situation
+    let chip_path = "/dev/gpiochip0";
+    let line_offsets = [5, 3, 7];
+
+    let mut lsettings = line::Settings::new()?;
+    lsettings.set_edge_detection(Some(line::Edge::Both))?;
+
+    let mut lconfig = line::Config::new()?;
+    lconfig.add_line_settings(&line_offsets, lsettings)?;
+
+    let mut rconfig = request::Config::new()?;
+    rconfig.set_consumer("watch-multiple-line-values")?;
+
+    let chip = libgpiod::chip::Chip::open(&chip_path)?;
+    let request = chip.request_lines(Some(&rconfig), &lconfig)?;
+
+    let mut buffer = request::Buffer::new(4)?;
+    loop {
+        // Blocks until at least one event is available.
+        let events = request.read_edge_events(&mut buffer)?;
+        for event in events {
+            let event = event?;
+            println!(
+                "offset: {}  type: {:<7}  event #{}  line event #{}",
+                event.line_offset(),
+                match event.event_type()? {
+                    EdgeKind::Rising => "Rising",
+                    EdgeKind::Falling => "Falling",
+                },
+                event.global_seqno(),
+                event.line_seqno(),
+            );
+        }
+    }
+}
diff --git a/bindings/rust/libgpiod/src/Makefile.am b/bindings/rust/libgpiod/src/Makefile.am
new file mode 100644 (file)
index 0000000..5892600
--- /dev/null
@@ -0,0 +1,15 @@
+# SPDX-License-Identifier: GPL-2.0-or-later
+# SPDX-FileCopyrightText: 2022 Linaro Ltd.
+# SPDX-FileCopyrightText: 2022 Bartosz Golaszewski <bartosz.golaszewski@linaro.org>
+
+EXTRA_DIST = \
+       chip.rs \
+       edge_event.rs \
+       event_buffer.rs \
+       info_event.rs \
+       lib.rs \
+       line_config.rs \
+       line_info.rs \
+       line_request.rs \
+       line_settings.rs \
+       request_config.rs
diff --git a/bindings/rust/libgpiod/src/chip.rs b/bindings/rust/libgpiod/src/chip.rs
new file mode 100644 (file)
index 0000000..bbb962f
--- /dev/null
@@ -0,0 +1,301 @@
+// SPDX-License-Identifier: Apache-2.0 OR BSD-3-Clause
+// SPDX-FileCopyrightText: 2022 Linaro Ltd.
+// SPDX-FileCopyrightText: 2022 Viresh Kumar <viresh.kumar@linaro.org>
+
+pub mod info {
+    /// GPIO chip info event related definitions.
+    pub use crate::info_event::*;
+}
+
+use std::cmp::Ordering;
+use std::ffi::{CStr, CString};
+use std::os::{raw::c_char, unix::prelude::AsRawFd};
+use std::path::Path;
+use std::ptr;
+use std::str;
+use std::time::Duration;
+
+use super::{
+    gpiod,
+    line::{self, Offset},
+    request, Error, OperationType, Result,
+};
+
+/// GPIO chip
+///
+/// A GPIO chip object is associated with an open file descriptor to the GPIO
+/// character device. It exposes basic information about the chip and allows
+/// callers to retrieve information about each line, watch lines for state
+/// changes and make line requests.
+#[derive(Debug, Eq, PartialEq)]
+pub struct Chip {
+    chip: *mut gpiod::gpiod_chip,
+}
+
+// SAFETY: Safe as chip is modeling a owned chip instance that may be freely
+// moved to other threads
+unsafe impl Send for Chip {}
+
+impl Chip {
+    /// Find a chip by path.
+    pub fn open<P: AsRef<Path>>(path: &P) -> Result<Self> {
+        // Null-terminate the string
+        let path = path.as_ref().to_string_lossy() + "\0";
+
+        // SAFETY: The `gpiod_chip` returned by libgpiod is guaranteed to live as long
+        // as the `struct Internal`.
+        let chip = unsafe { gpiod::gpiod_chip_open(path.as_ptr() as *const c_char) };
+        if chip.is_null() {
+            return Err(Error::OperationFailed(
+                OperationType::ChipOpen,
+                errno::errno(),
+            ));
+        }
+
+        Ok(Self { chip })
+    }
+
+    /// Get the chip name as represented in the kernel.
+    pub fn info(&self) -> Result<Info> {
+        Info::new(self)
+    }
+
+    /// Get the path used to find the chip.
+    pub fn path(&self) -> Result<&str> {
+        // SAFETY: The string returned by libgpiod is guaranteed to live as long
+        // as the `struct Chip`.
+        let path = unsafe { gpiod::gpiod_chip_get_path(self.chip) };
+
+        // SAFETY: The string is guaranteed to be valid here by the C API.
+        unsafe { CStr::from_ptr(path) }
+            .to_str()
+            .map_err(Error::StringNotUtf8)
+    }
+
+    /// Get a snapshot of information about the line.
+    pub fn line_info(&self, offset: Offset) -> Result<line::Info> {
+        // SAFETY: The `gpiod_line_info` returned by libgpiod is guaranteed to live as long
+        // as the `struct Info`.
+        let info = unsafe { gpiod::gpiod_chip_get_line_info(self.chip, offset) };
+
+        if info.is_null() {
+            return Err(Error::OperationFailed(
+                OperationType::ChipGetLineInfo,
+                errno::errno(),
+            ));
+        }
+
+        // SAFETY: We verified that the pointer is valid. We own the pointer and
+        // no longer use it after converting it into a Info instance.
+        Ok(unsafe { line::Info::from_raw(info) })
+    }
+
+    /// Get the current snapshot of information about the line at given offset and start watching
+    /// it for future changes.
+    pub fn watch_line_info(&self, offset: Offset) -> Result<line::Info> {
+        // SAFETY: `gpiod_line_info` is guaranteed to be valid here.
+        let info = unsafe { gpiod::gpiod_chip_watch_line_info(self.chip, offset) };
+
+        if info.is_null() {
+            return Err(Error::OperationFailed(
+                OperationType::ChipWatchLineInfo,
+                errno::errno(),
+            ));
+        }
+
+        // SAFETY: We verified that the pointer is valid. We own the instance and
+        // no longer use it after converting it into a Info instance.
+        Ok(unsafe { line::Info::from_raw(info) })
+    }
+
+    /// Stop watching a line
+    pub fn unwatch(&self, offset: Offset) {
+        // SAFETY: `gpiod_chip` is guaranteed to be valid here.
+        unsafe {
+            gpiod::gpiod_chip_unwatch_line_info(self.chip, offset);
+        }
+    }
+
+    /// Wait for line status events on any of the watched lines on the chip.
+    pub fn wait_info_event(&self, timeout: Option<Duration>) -> Result<bool> {
+        let timeout = match timeout {
+            Some(x) => x.as_nanos() as i64,
+            // Block indefinitely
+            None => -1,
+        };
+
+        // SAFETY: `gpiod_chip` is guaranteed to be valid here.
+        let ret = unsafe { gpiod::gpiod_chip_wait_info_event(self.chip, timeout) };
+
+        match ret {
+            -1 => Err(Error::OperationFailed(
+                OperationType::ChipWaitInfoEvent,
+                errno::errno(),
+            )),
+            0 => Ok(false),
+            _ => Ok(true),
+        }
+    }
+
+    /// Read a single line status change event from the chip. If no events are
+    /// pending, this function will block.
+    pub fn read_info_event(&self) -> Result<info::Event> {
+        // SAFETY: The `gpiod_info_event` returned by libgpiod is guaranteed to live as long
+        // as the `struct Event`.
+        let event = unsafe { gpiod::gpiod_chip_read_info_event(self.chip) };
+        if event.is_null() {
+            return Err(Error::OperationFailed(
+                OperationType::ChipReadInfoEvent,
+                errno::errno(),
+            ));
+        }
+
+        Ok(info::Event::new(event))
+    }
+
+    /// Map a GPIO line's name to its offset within the chip.
+    pub fn line_offset_from_name(&self, name: &str) -> Result<Offset> {
+        let name = CString::new(name).map_err(|_| Error::InvalidString)?;
+
+        // SAFETY: `gpiod_chip` is guaranteed to be valid here.
+        let ret = unsafe {
+            gpiod::gpiod_chip_get_line_offset_from_name(self.chip, name.as_ptr() as *const c_char)
+        };
+
+        if ret == -1 {
+            Err(Error::OperationFailed(
+                OperationType::ChipGetLineOffsetFromName,
+                errno::errno(),
+            ))
+        } else {
+            Ok(ret as u32)
+        }
+    }
+
+    /// Request a set of lines for exclusive usage.
+    pub fn request_lines(
+        &self,
+        rconfig: Option<&request::Config>,
+        lconfig: &line::Config,
+    ) -> Result<request::Request> {
+        let req_cfg = match rconfig {
+            Some(cfg) => cfg.config,
+            _ => ptr::null(),
+        } as *mut gpiod::gpiod_request_config;
+
+        // SAFETY: The `gpiod_line_request` returned by libgpiod is guaranteed to live as long
+        // as the `struct Request`.
+        let request =
+            unsafe { gpiod::gpiod_chip_request_lines(self.chip, req_cfg, lconfig.config) };
+
+        if request.is_null() {
+            return Err(Error::OperationFailed(
+                OperationType::ChipRequestLines,
+                errno::errno(),
+            ));
+        }
+
+        request::Request::new(request)
+    }
+}
+
+impl Drop for Chip {
+    /// Close the chip and release all associated resources.
+    fn drop(&mut self) {
+        // SAFETY: `gpiod_chip` is guaranteed to be valid here.
+        unsafe { gpiod::gpiod_chip_close(self.chip) }
+    }
+}
+
+impl AsRawFd for Chip {
+    /// Get the file descriptor associated with the chip.
+    ///
+    /// The returned file descriptor must not be closed by the caller, else other methods for the
+    /// `struct Chip` may fail.
+    fn as_raw_fd(&self) -> i32 {
+        // SAFETY: `gpiod_chip` is guaranteed to be valid here.
+        unsafe { gpiod::gpiod_chip_get_fd(self.chip) }
+    }
+}
+
+/// GPIO chip Information
+#[derive(Debug, Eq)]
+pub struct Info {
+    info: *mut gpiod::gpiod_chip_info,
+}
+
+impl Info {
+    /// Find a GPIO chip by path.
+    fn new(chip: &Chip) -> Result<Self> {
+        // SAFETY: `chip.chip` is guaranteed to be valid here.
+        let info = unsafe { gpiod::gpiod_chip_get_info(chip.chip) };
+        if info.is_null() {
+            return Err(Error::OperationFailed(
+                OperationType::ChipGetInfo,
+                errno::errno(),
+            ));
+        }
+
+        Ok(Self { info })
+    }
+
+    /// Get the GPIO chip name as represented in the kernel.
+    pub fn name(&self) -> Result<&str> {
+        // SAFETY: The string returned by libgpiod is guaranteed to live as long
+        // as the `struct Chip`.
+        let name = unsafe { gpiod::gpiod_chip_info_get_name(self.info) };
+
+        // SAFETY: The string is guaranteed to be valid here by the C API.
+        unsafe { CStr::from_ptr(name) }
+            .to_str()
+            .map_err(Error::StringNotUtf8)
+    }
+
+    /// Get the GPIO chip label as represented in the kernel.
+    pub fn label(&self) -> Result<&str> {
+        // SAFETY: The string returned by libgpiod is guaranteed to live as long
+        // as the `struct Chip`.
+        let label = unsafe { gpiod::gpiod_chip_info_get_label(self.info) };
+
+        // SAFETY: The string is guaranteed to be valid here by the C API.
+        unsafe { CStr::from_ptr(label) }
+            .to_str()
+            .map_err(Error::StringNotUtf8)
+    }
+
+    /// Get the number of GPIO lines exposed by the chip.
+    pub fn num_lines(&self) -> usize {
+        // SAFETY: `gpiod_chip` is guaranteed to be valid here.
+        unsafe { gpiod::gpiod_chip_info_get_num_lines(self.info) }
+    }
+}
+
+impl PartialEq for Info {
+    fn eq(&self, other: &Self) -> bool {
+        self.name().unwrap().eq(other.name().unwrap())
+    }
+}
+
+impl PartialOrd for Info {
+    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
+        let name = match self.name() {
+            Ok(name) => name,
+            _ => return None,
+        };
+
+        let other_name = match other.name() {
+            Ok(name) => name,
+            _ => return None,
+        };
+
+        name.partial_cmp(other_name)
+    }
+}
+
+impl Drop for Info {
+    /// Close the GPIO chip info and release all associated resources.
+    fn drop(&mut self) {
+        // SAFETY: `gpiod_chip` is guaranteed to be valid here.
+        unsafe { gpiod::gpiod_chip_info_free(self.info) }
+    }
+}
diff --git a/bindings/rust/libgpiod/src/edge_event.rs b/bindings/rust/libgpiod/src/edge_event.rs
new file mode 100644 (file)
index 0000000..7f8f377
--- /dev/null
@@ -0,0 +1,97 @@
+// SPDX-License-Identifier: Apache-2.0 OR BSD-3-Clause
+// SPDX-FileCopyrightText: 2022 Linaro Ltd.
+// SPDX-FileCopyrightText: 2022 Viresh Kumar <viresh.kumar@linaro.org>
+
+use std::time::Duration;
+
+use super::{
+    gpiod,
+    line::{EdgeKind, Offset},
+    Error, OperationType, Result,
+};
+
+/// Line edge events handling
+///
+/// An edge event object contains information about a single line edge event.
+/// It contains the event type, timestamp and the offset of the line on which
+/// the event occurred as well as two sequence numbers (global for all lines
+/// in the associated request and local for this line only).
+///
+/// Edge events are stored into an edge-event buffer object to improve
+/// performance and to limit the number of memory allocations when a large
+/// number of events are being read.
+
+#[derive(Debug, Eq, PartialEq)]
+pub struct Event(*mut gpiod::gpiod_edge_event);
+
+// SAFETY: Event models a wrapper around an owned gpiod_edge_event and may
+// be safely sent to other threads.
+unsafe impl Send for Event {}
+
+impl Event {
+    /// Makes a copy of the event object.
+    pub fn try_clone(event: &Event) -> Result<Event> {
+        // SAFETY: `gpiod_edge_event` is guaranteed to be valid here.
+        let event = unsafe { gpiod::gpiod_edge_event_copy(event.0) };
+        if event.is_null() {
+            return Err(Error::OperationFailed(
+                OperationType::EdgeEventCopy,
+                errno::errno(),
+            ));
+        }
+
+        Ok(Self(event))
+    }
+
+    /// Get the event type.
+    pub fn event_type(&self) -> Result<EdgeKind> {
+        // SAFETY: `gpiod_edge_event` is guaranteed to be valid here.
+        EdgeKind::new(unsafe { gpiod::gpiod_edge_event_get_event_type(self.0) })
+    }
+
+    /// Get the timestamp of the event.
+    pub fn timestamp(&self) -> Duration {
+        // SAFETY: `gpiod_edge_event` is guaranteed to be valid here.
+        Duration::from_nanos(unsafe { gpiod::gpiod_edge_event_get_timestamp_ns(self.0) })
+    }
+
+    /// Get the offset of the line on which the event was triggered.
+    pub fn line_offset(&self) -> Offset {
+        // SAFETY: `gpiod_edge_event` is guaranteed to be valid here.
+        unsafe { gpiod::gpiod_edge_event_get_line_offset(self.0) }
+    }
+
+    /// Get the global sequence number of the event.
+    ///
+    /// Returns sequence number of the event relative to all lines in the
+    /// associated line request.
+    pub fn global_seqno(&self) -> usize {
+        // SAFETY: `gpiod_edge_event` is guaranteed to be valid here.
+        unsafe {
+            gpiod::gpiod_edge_event_get_global_seqno(self.0)
+                .try_into()
+                .unwrap()
+        }
+    }
+
+    /// Get the event sequence number specific to concerned line.
+    ///
+    /// Returns sequence number of the event relative to the line within the
+    /// lifetime of the associated line request.
+    pub fn line_seqno(&self) -> usize {
+        // SAFETY: `gpiod_edge_event` is guaranteed to be valid here.
+        unsafe {
+            gpiod::gpiod_edge_event_get_line_seqno(self.0)
+                .try_into()
+                .unwrap()
+        }
+    }
+}
+
+impl Drop for Event {
+    /// Free the edge event.
+    fn drop(&mut self) {
+        // SAFETY: `gpiod_edge_event` is guaranteed to be valid here.
+        unsafe { gpiod::gpiod_edge_event_free(self.0) };
+    }
+}
diff --git a/bindings/rust/libgpiod/src/event_buffer.rs b/bindings/rust/libgpiod/src/event_buffer.rs
new file mode 100644 (file)
index 0000000..68d6e2f
--- /dev/null
@@ -0,0 +1,178 @@
+// SPDX-License-Identifier: Apache-2.0 OR BSD-3-Clause
+// SPDX-FileCopyrightText: 2022 Linaro Ltd.
+// SPDX-FileCopyrightText: 2022 Viresh Kumar <viresh.kumar@linaro.org>
+
+use std::ptr;
+
+use super::{
+    gpiod,
+    request::{Event, Request},
+    Error, OperationType, Result,
+};
+
+/// Line edge events
+///
+/// An iterator over the elements of type `Event`.
+
+pub struct Events<'a> {
+    buffer: &'a mut Buffer,
+    read_index: usize,
+    len: usize,
+}
+
+impl<'a> Events<'a> {
+    pub fn new(buffer: &'a mut Buffer, len: usize) -> Self {
+        Self {
+            buffer,
+            read_index: 0,
+            len,
+        }
+    }
+
+    /// Get the number of contained events in the snapshot, this doesn't change
+    /// on reading events from the iterator.
+    pub fn len(&self) -> usize {
+        self.len
+    }
+
+    /// Check if buffer is empty.
+    pub fn is_empty(&self) -> bool {
+        self.len == 0
+    }
+}
+
+impl<'a> Iterator for Events<'a> {
+    type Item = Result<&'a Event>;
+
+    fn nth(&mut self, n: usize) -> Option<Self::Item> {
+        if self.read_index + n >= self.len {
+            return None;
+        }
+
+        self.read_index += n + 1;
+        Some(self.buffer.event(self.read_index - 1))
+    }
+
+    fn next(&mut self) -> Option<Self::Item> {
+        // clippy false-positive, fixed in next clippy release:
+        // https://github.com/rust-lang/rust-clippy/issues/9820
+        #[allow(clippy::iter_nth_zero)]
+        self.nth(0)
+    }
+}
+
+/// Line edge events buffer
+#[derive(Debug, Eq, PartialEq)]
+pub struct Buffer {
+    pub(crate) buffer: *mut gpiod::gpiod_edge_event_buffer,
+    events: Vec<*mut gpiod::gpiod_edge_event>,
+}
+
+// SAFETY: Buffer models an owned gpiod_edge_event_buffer. However, there may
+// be events tied to it. Concurrent access from multiple threads to a buffer
+// and its associated events is not allowed by the C lib.
+// In Rust, those events will always be borrowed from a buffer instance. Thus,
+// either Rust prevents the user to move the Buffer while there are still
+// borrowed events, or we can safely send the the Buffer.
+unsafe impl Send for Buffer {}
+
+impl Buffer {
+    /// Create a new edge event buffer.
+    ///
+    /// If capacity equals 0, it will be set to a default value of 64. If
+    /// capacity is larger than 1024, it will be limited to 1024.
+    pub fn new(capacity: usize) -> Result<Self> {
+        // SAFETY: The `gpiod_edge_event_buffer` returned by libgpiod is guaranteed to live as long
+        // as the `struct Buffer`.
+        let buffer = unsafe { gpiod::gpiod_edge_event_buffer_new(capacity) };
+        if buffer.is_null() {
+            return Err(Error::OperationFailed(
+                OperationType::EdgeEventBufferNew,
+                errno::errno(),
+            ));
+        }
+
+        // SAFETY: `gpiod_edge_event_buffer` is guaranteed to be valid here.
+        let capacity = unsafe { gpiod::gpiod_edge_event_buffer_get_capacity(buffer) };
+
+        Ok(Self {
+            buffer,
+            events: vec![ptr::null_mut(); capacity],
+        })
+    }
+
+    /// Get the capacity of the event buffer.
+    pub fn capacity(&self) -> usize {
+        self.events.len()
+    }
+
+    /// Get edge events from a line request.
+    ///
+    /// This function will block if no event was queued for the line.
+    pub fn read_edge_events(&mut self, request: &Request) -> Result<Events> {
+        for i in 0..self.events.len() {
+            self.events[i] = ptr::null_mut();
+        }
+
+        // SAFETY: `gpiod_line_request` is guaranteed to be valid here.
+        let ret = unsafe {
+            gpiod::gpiod_line_request_read_edge_events(
+                request.request,
+                self.buffer,
+                self.events.len(),
+            )
+        };
+
+        if ret == -1 {
+            Err(Error::OperationFailed(
+                OperationType::LineRequestReadEdgeEvent,
+                errno::errno(),
+            ))
+        } else {
+            let ret = ret as usize;
+
+            if ret > self.events.len() {
+                Err(Error::TooManyEvents(ret, self.events.len()))
+            } else {
+                Ok(Events::new(self, ret))
+            }
+        }
+    }
+
+    /// Read an event stored in the buffer.
+    fn event<'a>(&mut self, index: usize) -> Result<&'a Event> {
+        if self.events[index].is_null() {
+            // SAFETY: The `gpiod_edge_event` returned by libgpiod is guaranteed to live as long
+            // as the `struct Event`.
+            let event = unsafe {
+                gpiod::gpiod_edge_event_buffer_get_event(self.buffer, index.try_into().unwrap())
+            };
+
+            if event.is_null() {
+                return Err(Error::OperationFailed(
+                    OperationType::EdgeEventBufferGetEvent,
+                    errno::errno(),
+                ));
+            }
+
+            self.events[index] = event;
+        }
+
+        // SAFETY: Safe as the underlying events object won't get freed until the time the returned
+        // reference is still used.
+        Ok(unsafe {
+            // This will not lead to `drop(event)`.
+            (self.events.as_ptr().add(index) as *const Event)
+                .as_ref()
+                .unwrap()
+        })
+    }
+}
+
+impl Drop for Buffer {
+    /// Free the edge event buffer and release all associated resources.
+    fn drop(&mut self) {
+        // SAFETY: `gpiod_edge_event_buffer` is guaranteed to be valid here.
+        unsafe { gpiod::gpiod_edge_event_buffer_free(self.buffer) };
+    }
+}
diff --git a/bindings/rust/libgpiod/src/info_event.rs b/bindings/rust/libgpiod/src/info_event.rs
new file mode 100644 (file)
index 0000000..472c891
--- /dev/null
@@ -0,0 +1,76 @@
+// SPDX-License-Identifier: Apache-2.0 OR BSD-3-Clause
+// SPDX-FileCopyrightText: 2022 Linaro Ltd.
+// SPDX-FileCopyrightText: 2022 Viresh Kumar <viresh.kumar@linaro.org>
+
+use std::time::Duration;
+
+use super::{
+    gpiod,
+    line::{self, InfoChangeKind},
+    Error, OperationType, Result,
+};
+
+/// Line status watch events
+///
+/// Accessors for the info event objects allowing to monitor changes in GPIO
+/// line state.
+///
+/// Callers can be notified about changes in line's state using the interfaces
+/// exposed by GPIO chips. Each info event contains information about the event
+/// itself (timestamp, type) as well as a snapshot of line's state in the form
+/// of a line-info object.
+
+#[derive(Debug, Eq, PartialEq)]
+pub struct Event {
+    pub(crate) event: *mut gpiod::gpiod_info_event,
+}
+
+// SAFETY: Event models a wrapper around an owned gpiod_info_event and may be
+// safely sent to other threads.
+unsafe impl Send for Event {}
+
+impl Event {
+    /// Get a single chip's line's status change event.
+    pub(crate) fn new(event: *mut gpiod::gpiod_info_event) -> Self {
+        Self { event }
+    }
+
+    /// Get the event type of the status change event.
+    pub fn event_type(&self) -> Result<InfoChangeKind> {
+        // SAFETY: `gpiod_info_event` is guaranteed to be valid here.
+        InfoChangeKind::new(unsafe { gpiod::gpiod_info_event_get_event_type(self.event) })
+    }
+
+    /// Get the timestamp of the event, read from the monotonic clock.
+    pub fn timestamp(&self) -> Duration {
+        // SAFETY: `gpiod_info_event` is guaranteed to be valid here.
+        Duration::from_nanos(unsafe { gpiod::gpiod_info_event_get_timestamp_ns(self.event) })
+    }
+
+    /// Get the line-info object associated with the event.
+    pub fn line_info(&self) -> Result<&line::InfoRef> {
+        // SAFETY: `gpiod_line_info` is guaranteed to be valid here.
+        let info = unsafe { gpiod::gpiod_info_event_get_line_info(self.event) };
+
+        if info.is_null() {
+            return Err(Error::OperationFailed(
+                OperationType::InfoEventGetLineInfo,
+                errno::errno(),
+            ));
+        }
+
+        // SAFETY: The pointer is valid. The returned reference receives the
+        // lifetime '0 - the same as &self. &self also controls lifetime and
+        // ownership of the owning object. Therefore, the borrow prevents moving
+        // of the owning object to another thread.
+        Ok(unsafe { line::InfoRef::from_raw(info) })
+    }
+}
+
+impl Drop for Event {
+    /// Free the info event object and release all associated resources.
+    fn drop(&mut self) {
+        // SAFETY: `gpiod_info_event` is guaranteed to be valid here.
+        unsafe { gpiod::gpiod_info_event_free(self.event) }
+    }
+}
diff --git a/bindings/rust/libgpiod/src/lib.rs b/bindings/rust/libgpiod/src/lib.rs
new file mode 100644 (file)
index 0000000..fd95ed2
--- /dev/null
@@ -0,0 +1,518 @@
+// SPDX-License-Identifier: Apache-2.0 OR BSD-3-Clause
+// SPDX-FileCopyrightText: 2022 Linaro Ltd.
+// SPDX-FileCopyrightText: 2022 Viresh Kumar <viresh.kumar@linaro.org>
+//
+// Rust wrappers for GPIOD APIs
+
+//! libgpiod public API
+//!
+//! This is the complete documentation of the public Rust API made available to
+//! users of libgpiod.
+//!
+//! The API is logically split into several parts such as: GPIO chip & line
+//! operators, GPIO events handling etc.
+
+use std::ffi::CStr;
+use std::fs;
+use std::os::raw::c_char;
+use std::path::Path;
+use std::time::Duration;
+use std::{fmt, str};
+
+use intmap::IntMap;
+use thiserror::Error as ThisError;
+
+use libgpiod_sys as gpiod;
+
+use gpiod::{
+    gpiod_edge_event_type_GPIOD_EDGE_EVENT_FALLING_EDGE as GPIOD_EDGE_EVENT_FALLING_EDGE,
+    gpiod_edge_event_type_GPIOD_EDGE_EVENT_RISING_EDGE as GPIOD_EDGE_EVENT_RISING_EDGE,
+    gpiod_info_event_type_GPIOD_INFO_EVENT_LINE_CONFIG_CHANGED as GPIOD_INFO_EVENT_LINE_CONFIG_CHANGED,
+    gpiod_info_event_type_GPIOD_INFO_EVENT_LINE_RELEASED as GPIOD_INFO_EVENT_LINE_RELEASED,
+    gpiod_info_event_type_GPIOD_INFO_EVENT_LINE_REQUESTED as GPIOD_INFO_EVENT_LINE_REQUESTED,
+    gpiod_line_bias_GPIOD_LINE_BIAS_AS_IS as GPIOD_LINE_BIAS_AS_IS,
+    gpiod_line_bias_GPIOD_LINE_BIAS_DISABLED as GPIOD_LINE_BIAS_DISABLED,
+    gpiod_line_bias_GPIOD_LINE_BIAS_PULL_DOWN as GPIOD_LINE_BIAS_PULL_DOWN,
+    gpiod_line_bias_GPIOD_LINE_BIAS_PULL_UP as GPIOD_LINE_BIAS_PULL_UP,
+    gpiod_line_bias_GPIOD_LINE_BIAS_UNKNOWN as GPIOD_LINE_BIAS_UNKNOWN,
+    gpiod_line_clock_GPIOD_LINE_CLOCK_HTE as GPIOD_LINE_CLOCK_HTE,
+    gpiod_line_clock_GPIOD_LINE_CLOCK_MONOTONIC as GPIOD_LINE_CLOCK_MONOTONIC,
+    gpiod_line_clock_GPIOD_LINE_CLOCK_REALTIME as GPIOD_LINE_CLOCK_REALTIME,
+    gpiod_line_direction_GPIOD_LINE_DIRECTION_AS_IS as GPIOD_LINE_DIRECTION_AS_IS,
+    gpiod_line_direction_GPIOD_LINE_DIRECTION_INPUT as GPIOD_LINE_DIRECTION_INPUT,
+    gpiod_line_direction_GPIOD_LINE_DIRECTION_OUTPUT as GPIOD_LINE_DIRECTION_OUTPUT,
+    gpiod_line_drive_GPIOD_LINE_DRIVE_OPEN_DRAIN as GPIOD_LINE_DRIVE_OPEN_DRAIN,
+    gpiod_line_drive_GPIOD_LINE_DRIVE_OPEN_SOURCE as GPIOD_LINE_DRIVE_OPEN_SOURCE,
+    gpiod_line_drive_GPIOD_LINE_DRIVE_PUSH_PULL as GPIOD_LINE_DRIVE_PUSH_PULL,
+    gpiod_line_edge_GPIOD_LINE_EDGE_BOTH as GPIOD_LINE_EDGE_BOTH,
+    gpiod_line_edge_GPIOD_LINE_EDGE_FALLING as GPIOD_LINE_EDGE_FALLING,
+    gpiod_line_edge_GPIOD_LINE_EDGE_NONE as GPIOD_LINE_EDGE_NONE,
+    gpiod_line_edge_GPIOD_LINE_EDGE_RISING as GPIOD_LINE_EDGE_RISING,
+    gpiod_line_value_GPIOD_LINE_VALUE_ACTIVE as GPIOD_LINE_VALUE_ACTIVE,
+    gpiod_line_value_GPIOD_LINE_VALUE_ERROR as GPIOD_LINE_VALUE_ERROR,
+    gpiod_line_value_GPIOD_LINE_VALUE_INACTIVE as GPIOD_LINE_VALUE_INACTIVE,
+};
+
+/// Operation types, used with OperationFailed() Error.
+#[derive(Copy, Clone, Debug, Eq, PartialEq)]
+pub enum OperationType {
+    ChipOpen,
+    ChipWaitInfoEvent,
+    ChipGetLine,
+    ChipGetLineInfo,
+    ChipGetLineOffsetFromName,
+    ChipGetInfo,
+    ChipReadInfoEvent,
+    ChipRequestLines,
+    ChipWatchLineInfo,
+    EdgeEventBufferGetEvent,
+    EdgeEventCopy,
+    EdgeEventBufferNew,
+    InfoEventGetLineInfo,
+    LineConfigNew,
+    LineConfigAddSettings,
+    LineConfigSetOutputValues,
+    LineConfigGetOffsets,
+    LineConfigGetSettings,
+    LineInfoCopy,
+    LineRequestReconfigLines,
+    LineRequestGetVal,
+    LineRequestGetValSubset,
+    LineRequestSetVal,
+    LineRequestSetValSubset,
+    LineRequestReadEdgeEvent,
+    LineRequestWaitEdgeEvent,
+    LineSettingsNew,
+    LineSettingsCopy,
+    LineSettingsGetOutVal,
+    LineSettingsSetDirection,
+    LineSettingsSetEdgeDetection,
+    LineSettingsSetBias,
+    LineSettingsSetDrive,
+    LineSettingsSetActiveLow,
+    LineSettingsSetDebouncePeriod,
+    LineSettingsSetEventClock,
+    LineSettingsSetOutputValue,
+    RequestConfigNew,
+    RequestConfigGetConsumer,
+    SimBankGetVal,
+    SimBankNew,
+    SimBankSetLabel,
+    SimBankSetNumLines,
+    SimBankSetLineName,
+    SimBankSetPull,
+    SimBankHogLine,
+    SimCtxNew,
+    SimDevNew,
+    SimDevEnable,
+    SimDevDisable,
+}
+
+impl fmt::Display for OperationType {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        write!(f, "{:?}", self)
+    }
+}
+
+/// Result of libgpiod operations.
+pub type Result<T> = std::result::Result<T, Error>;
+
+/// Error codes for libgpiod operations.
+#[derive(Copy, Clone, Debug, Eq, PartialEq, ThisError)]
+pub enum Error {
+    #[error("Failed to get {0}")]
+    NullString(&'static str),
+    #[error("String not utf8: {0:?}")]
+    StringNotUtf8(str::Utf8Error),
+    #[error("Invalid String")]
+    InvalidString,
+    #[error("Invalid enum {0} value: {1}")]
+    InvalidEnumValue(&'static str, i32),
+    #[error("Operation {0} Failed: {1}")]
+    OperationFailed(OperationType, errno::Errno),
+    #[error("Invalid Arguments")]
+    InvalidArguments,
+    #[error("Event count more than buffer capacity: {0} > {1}")]
+    TooManyEvents(usize, usize),
+    #[error("Std Io Error")]
+    IoError,
+}
+
+mod info_event;
+
+/// GPIO chip related definitions.
+pub mod chip;
+
+mod edge_event;
+mod event_buffer;
+mod line_request;
+mod request_config;
+
+/// GPIO chip request related definitions.
+pub mod request {
+    pub use crate::edge_event::*;
+    pub use crate::event_buffer::*;
+    pub use crate::line_request::*;
+    pub use crate::request_config::*;
+}
+
+mod line_config;
+mod line_info;
+mod line_settings;
+
+/// GPIO chip line related definitions.
+pub mod line {
+    pub use crate::line_config::*;
+    pub use crate::line_info::*;
+    pub use crate::line_settings::*;
+
+    use super::*;
+
+    /// Value settings.
+    #[derive(Copy, Clone, Debug, Eq, PartialEq)]
+    pub enum Value {
+        /// Active
+        Active,
+        /// Inactive
+        InActive,
+    }
+
+    /// Maps offset to Value.
+    pub type ValueMap = IntMap<Value>;
+
+    /// Maps offsets to Settings
+    pub type SettingsMap = IntMap<Settings>;
+
+    impl Value {
+        pub fn new(val: gpiod::gpiod_line_value) -> Result<Self> {
+            Ok(match val {
+                GPIOD_LINE_VALUE_INACTIVE => Value::InActive,
+                GPIOD_LINE_VALUE_ACTIVE => Value::Active,
+                GPIOD_LINE_VALUE_ERROR => {
+                    return Err(Error::OperationFailed(
+                        OperationType::LineRequestGetVal,
+                        errno::errno(),
+                    ))
+                }
+                _ => return Err(Error::InvalidEnumValue("Value", val)),
+            })
+        }
+
+        pub(crate) fn value(&self) -> gpiod::gpiod_line_value {
+            match self {
+                Value::Active => GPIOD_LINE_VALUE_ACTIVE,
+                Value::InActive => GPIOD_LINE_VALUE_INACTIVE,
+            }
+        }
+    }
+
+    /// Offset type.
+    pub type Offset = u32;
+
+    /// Direction settings.
+    #[derive(Copy, Clone, Debug, Eq, PartialEq)]
+    pub enum Direction {
+        /// Request the line(s), but don't change direction.
+        AsIs,
+        /// Direction is input - for reading the value of an externally driven GPIO line.
+        Input,
+        /// Direction is output - for driving the GPIO line.
+        Output,
+    }
+
+    impl Direction {
+        pub(crate) fn new(dir: gpiod::gpiod_line_direction) -> Result<Self> {
+            Ok(match dir {
+                GPIOD_LINE_DIRECTION_AS_IS => Direction::AsIs,
+                GPIOD_LINE_DIRECTION_INPUT => Direction::Input,
+                GPIOD_LINE_DIRECTION_OUTPUT => Direction::Output,
+                _ => return Err(Error::InvalidEnumValue("Direction", dir as i32)),
+            })
+        }
+
+        pub(crate) fn gpiod_direction(&self) -> gpiod::gpiod_line_direction {
+            match self {
+                Direction::AsIs => GPIOD_LINE_DIRECTION_AS_IS,
+                Direction::Input => GPIOD_LINE_DIRECTION_INPUT,
+                Direction::Output => GPIOD_LINE_DIRECTION_OUTPUT,
+            }
+        }
+    }
+
+    /// Internal bias settings.
+    #[derive(Copy, Clone, Debug, Eq, PartialEq)]
+    pub enum Bias {
+        /// The internal bias is disabled.
+        Disabled,
+        /// The internal pull-up bias is enabled.
+        PullUp,
+        /// The internal pull-down bias is enabled.
+        PullDown,
+    }
+
+    impl Bias {
+        pub(crate) fn new(bias: gpiod::gpiod_line_bias) -> Result<Option<Self>> {
+            Ok(match bias {
+                GPIOD_LINE_BIAS_UNKNOWN => None,
+                GPIOD_LINE_BIAS_AS_IS => None,
+                GPIOD_LINE_BIAS_DISABLED => Some(Bias::Disabled),
+                GPIOD_LINE_BIAS_PULL_UP => Some(Bias::PullUp),
+                GPIOD_LINE_BIAS_PULL_DOWN => Some(Bias::PullDown),
+                _ => return Err(Error::InvalidEnumValue("Bias", bias as i32)),
+            })
+        }
+
+        pub(crate) fn gpiod_bias(bias: Option<Bias>) -> gpiod::gpiod_line_bias {
+            match bias {
+                None => GPIOD_LINE_BIAS_AS_IS,
+                Some(bias) => match bias {
+                    Bias::Disabled => GPIOD_LINE_BIAS_DISABLED,
+                    Bias::PullUp => GPIOD_LINE_BIAS_PULL_UP,
+                    Bias::PullDown => GPIOD_LINE_BIAS_PULL_DOWN,
+                },
+            }
+        }
+    }
+
+    /// Drive settings.
+    #[derive(Copy, Clone, Debug, Eq, PartialEq)]
+    pub enum Drive {
+        /// Drive setting is push-pull.
+        PushPull,
+        /// Line output is open-drain.
+        OpenDrain,
+        /// Line output is open-source.
+        OpenSource,
+    }
+
+    impl Drive {
+        pub(crate) fn new(drive: gpiod::gpiod_line_drive) -> Result<Self> {
+            Ok(match drive {
+                GPIOD_LINE_DRIVE_PUSH_PULL => Drive::PushPull,
+                GPIOD_LINE_DRIVE_OPEN_DRAIN => Drive::OpenDrain,
+                GPIOD_LINE_DRIVE_OPEN_SOURCE => Drive::OpenSource,
+                _ => return Err(Error::InvalidEnumValue("Drive", drive as i32)),
+            })
+        }
+
+        pub(crate) fn gpiod_drive(&self) -> gpiod::gpiod_line_drive {
+            match self {
+                Drive::PushPull => GPIOD_LINE_DRIVE_PUSH_PULL,
+                Drive::OpenDrain => GPIOD_LINE_DRIVE_OPEN_DRAIN,
+                Drive::OpenSource => GPIOD_LINE_DRIVE_OPEN_SOURCE,
+            }
+        }
+    }
+
+    /// Edge detection settings.
+    #[derive(Copy, Clone, Debug, Eq, PartialEq)]
+    pub enum Edge {
+        /// Line detects rising edge events.
+        Rising,
+        /// Line detects falling edge events.
+        Falling,
+        /// Line detects both rising and falling edge events.
+        Both,
+    }
+
+    impl Edge {
+        pub(crate) fn new(edge: gpiod::gpiod_line_edge) -> Result<Option<Self>> {
+            Ok(match edge {
+                GPIOD_LINE_EDGE_NONE => None,
+                GPIOD_LINE_EDGE_RISING => Some(Edge::Rising),
+                GPIOD_LINE_EDGE_FALLING => Some(Edge::Falling),
+                GPIOD_LINE_EDGE_BOTH => Some(Edge::Both),
+                _ => return Err(Error::InvalidEnumValue("Edge", edge as i32)),
+            })
+        }
+
+        pub(crate) fn gpiod_edge(edge: Option<Edge>) -> gpiod::gpiod_line_edge {
+            match edge {
+                None => GPIOD_LINE_EDGE_NONE,
+                Some(edge) => match edge {
+                    Edge::Rising => GPIOD_LINE_EDGE_RISING,
+                    Edge::Falling => GPIOD_LINE_EDGE_FALLING,
+                    Edge::Both => GPIOD_LINE_EDGE_BOTH,
+                },
+            }
+        }
+    }
+
+    /// Line setting kind.
+    #[derive(Copy, Clone, Debug, Eq, PartialEq)]
+    pub enum SettingKind {
+        /// Line direction.
+        Direction,
+        /// Bias.
+        Bias,
+        /// Drive.
+        Drive,
+        /// Edge detection.
+        EdgeDetection,
+        /// Active-low setting.
+        ActiveLow,
+        /// Debounce period.
+        DebouncePeriod,
+        /// Event clock type.
+        EventClock,
+        /// Output value.
+        OutputValue,
+    }
+
+    /// Line settings.
+    #[derive(Copy, Clone, Debug, Eq, PartialEq)]
+    pub enum SettingVal {
+        /// Line direction.
+        Direction(Direction),
+        /// Bias.
+        Bias(Option<Bias>),
+        /// Drive.
+        Drive(Drive),
+        /// Edge detection.
+        EdgeDetection(Option<Edge>),
+        /// Active-low setting.
+        ActiveLow(bool),
+        /// Debounce period.
+        DebouncePeriod(Duration),
+        /// Event clock type.
+        EventClock(EventClock),
+        /// Output value.
+        OutputValue(Value),
+    }
+
+    impl fmt::Display for SettingVal {
+        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+            write!(f, "{:?}", self)
+        }
+    }
+
+    /// Event clock settings.
+    #[derive(Copy, Clone, Debug, Eq, PartialEq)]
+    pub enum EventClock {
+        /// Line uses the monotonic clock for edge event timestamps.
+        Monotonic,
+        /// Line uses the realtime clock for edge event timestamps.
+        Realtime,
+        /// Line uses the hardware timestamp engine clock for edge event timestamps.
+        HTE,
+    }
+
+    impl EventClock {
+        pub(crate) fn new(clock: gpiod::gpiod_line_clock) -> Result<Self> {
+            Ok(match clock {
+                GPIOD_LINE_CLOCK_MONOTONIC => EventClock::Monotonic,
+                GPIOD_LINE_CLOCK_REALTIME => EventClock::Realtime,
+                GPIOD_LINE_CLOCK_HTE => EventClock::HTE,
+                _ => return Err(Error::InvalidEnumValue("Eventclock", clock as i32)),
+            })
+        }
+
+        pub(crate) fn gpiod_clock(&self) -> gpiod::gpiod_line_clock {
+            match self {
+                EventClock::Monotonic => GPIOD_LINE_CLOCK_MONOTONIC,
+                EventClock::Realtime => GPIOD_LINE_CLOCK_REALTIME,
+                EventClock::HTE => GPIOD_LINE_CLOCK_HTE,
+            }
+        }
+    }
+
+    /// Line status change event types.
+    #[derive(Copy, Clone, Debug, Eq, PartialEq)]
+    pub enum InfoChangeKind {
+        /// Line has been requested.
+        LineRequested,
+        /// Previously requested line has been released.
+        LineReleased,
+        /// Line configuration has changed.
+        LineConfigChanged,
+    }
+
+    impl InfoChangeKind {
+        pub(crate) fn new(kind: gpiod::gpiod_info_event_type) -> Result<Self> {
+            Ok(match kind {
+                GPIOD_INFO_EVENT_LINE_REQUESTED => InfoChangeKind::LineRequested,
+                GPIOD_INFO_EVENT_LINE_RELEASED => InfoChangeKind::LineReleased,
+                GPIOD_INFO_EVENT_LINE_CONFIG_CHANGED => InfoChangeKind::LineConfigChanged,
+                _ => return Err(Error::InvalidEnumValue("InfoChangeKind", kind as i32)),
+            })
+        }
+    }
+
+    /// Edge event types.
+    #[derive(Copy, Clone, Debug, Eq, PartialEq)]
+    pub enum EdgeKind {
+        /// Rising edge event.
+        Rising,
+        /// Falling edge event.
+        Falling,
+    }
+
+    impl EdgeKind {
+        pub(crate) fn new(kind: gpiod::gpiod_edge_event_type) -> Result<Self> {
+            Ok(match kind {
+                GPIOD_EDGE_EVENT_RISING_EDGE => EdgeKind::Rising,
+                GPIOD_EDGE_EVENT_FALLING_EDGE => EdgeKind::Falling,
+                _ => return Err(Error::InvalidEnumValue("EdgeEvent", kind as i32)),
+            })
+        }
+    }
+}
+
+/// Various libgpiod-related functions.
+
+/// Check if the file pointed to by path is a GPIO chip character device.
+///
+/// Returns true if the file exists and is a GPIO chip character device or a
+/// symbolic link to it.
+pub fn is_gpiochip_device<P: AsRef<Path>>(path: &P) -> bool {
+    // Null-terminate the string
+    let path = path.as_ref().to_string_lossy() + "\0";
+
+    // SAFETY: libgpiod won't access the path reference once the call returns.
+    unsafe { gpiod::gpiod_is_gpiochip_device(path.as_ptr() as *const c_char) }
+}
+
+/// GPIO devices.
+///
+/// Returns a vector of unique available GPIO Chips.
+///
+/// The chips are sorted in ascending order of the chip names.
+pub fn gpiochip_devices<P: AsRef<Path>>(path: &P) -> Result<Vec<chip::Chip>> {
+    let mut devices = Vec::new();
+
+    for entry in fs::read_dir(path).map_err(|_| Error::IoError)?.flatten() {
+        let path = entry.path();
+
+        if is_gpiochip_device(&path) {
+            let chip = chip::Chip::open(&path)?;
+            let info = chip.info()?;
+
+            devices.push((chip, info));
+        }
+    }
+
+    devices.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap());
+    devices.dedup_by(|a, b| a.1.eq(&b.1));
+
+    Ok(devices.into_iter().map(|a| a.0).collect())
+}
+
+/// Get the API version of the libgpiod library as a human-readable string.
+pub fn libgpiod_version() -> Result<&'static str> {
+    // SAFETY: The string returned by libgpiod is guaranteed to live forever.
+    let version = unsafe { gpiod::gpiod_api_version() };
+
+    if version.is_null() {
+        return Err(Error::NullString("GPIO library version"));
+    }
+
+    // SAFETY: The string is guaranteed to be valid here by the C API.
+    unsafe { CStr::from_ptr(version) }
+        .to_str()
+        .map_err(Error::StringNotUtf8)
+}
+
+/// Get the API version of the libgpiod crate as a human-readable string.
+pub fn crate_version() -> &'static str {
+    env!("CARGO_PKG_VERSION")
+}
diff --git a/bindings/rust/libgpiod/src/line_config.rs b/bindings/rust/libgpiod/src/line_config.rs
new file mode 100644 (file)
index 0000000..d0a4aba
--- /dev/null
@@ -0,0 +1,150 @@
+// SPDX-License-Identifier: Apache-2.0 OR BSD-3-Clause
+// SPDX-FileCopyrightText: 2022 Linaro Ltd.
+// SPDX-FileCopyrightText: 2022 Viresh Kumar <viresh.kumar@linaro.org>
+
+use super::{
+    gpiod,
+    line::{Offset, Settings, SettingsMap, Value},
+    Error, OperationType, Result,
+};
+
+/// Line configuration objects.
+///
+/// The line-config object contains the configuration for lines that can be
+/// used in two cases:
+///  - when making a line request
+///  - when reconfiguring a set of already requested lines.
+///
+/// A new line-config object is empty. Using it in a request will lead to an
+/// error. In order for a line-config to become useful, it needs to be assigned
+/// at least one offset-to-settings mapping by calling
+/// ::gpiod_line_config_add_line_settings.
+///
+/// When calling ::gpiod_chip_request_lines, the library will request all
+/// offsets that were assigned settings in the order that they were assigned.
+
+#[derive(Debug, Eq, PartialEq)]
+pub struct Config {
+    pub(crate) config: *mut gpiod::gpiod_line_config,
+}
+
+// SAFETY: Config models a wrapper around an owned gpiod_line_config and may be
+// safely sent to other threads.
+unsafe impl Send for Config {}
+
+impl Config {
+    /// Create a new line config object.
+    pub fn new() -> Result<Self> {
+        // SAFETY: The `gpiod_line_config` returned by libgpiod is guaranteed to live as long
+        // as the `struct Config`.
+        let config = unsafe { gpiod::gpiod_line_config_new() };
+
+        if config.is_null() {
+            return Err(Error::OperationFailed(
+                OperationType::LineConfigNew,
+                errno::errno(),
+            ));
+        }
+
+        Ok(Self { config })
+    }
+
+    /// Resets the entire configuration stored in the object. This is useful if
+    /// the user wants to reuse the object without reallocating it.
+    pub fn reset(&mut self) {
+        // SAFETY: `gpiod_line_config` is guaranteed to be valid here.
+        unsafe { gpiod::gpiod_line_config_reset(self.config) }
+    }
+
+    /// Add line settings for a set of offsets.
+    pub fn add_line_settings(
+        &mut self,
+        offsets: &[Offset],
+        settings: Settings,
+    ) -> Result<&mut Self> {
+        // SAFETY: `gpiod_line_config` is guaranteed to be valid here.
+        let ret = unsafe {
+            gpiod::gpiod_line_config_add_line_settings(
+                self.config,
+                offsets.as_ptr(),
+                offsets.len(),
+                settings.settings,
+            )
+        };
+
+        if ret == -1 {
+            Err(Error::OperationFailed(
+                OperationType::LineConfigAddSettings,
+                errno::errno(),
+            ))
+        } else {
+            Ok(self)
+        }
+    }
+
+    /// Set output values for a number of lines.
+    pub fn set_output_values(&mut self, values: &[Value]) -> Result<&mut Self> {
+        let mut mapped_values = Vec::new();
+        for value in values {
+            mapped_values.push(value.value());
+        }
+
+        let ret = unsafe {
+            gpiod::gpiod_line_config_set_output_values(
+                self.config,
+                mapped_values.as_ptr(),
+                values.len(),
+            )
+        };
+
+        if ret == -1 {
+            Err(Error::OperationFailed(
+                OperationType::LineConfigSetOutputValues,
+                errno::errno(),
+            ))
+        } else {
+            Ok(self)
+        }
+    }
+
+    /// Get a mapping of offsets to line settings stored by this object.
+    pub fn line_settings(&self) -> Result<SettingsMap> {
+        let mut map = SettingsMap::new();
+        // SAFETY: gpiod_line_config is guaranteed to be valid here
+        let num_lines = unsafe { gpiod::gpiod_line_config_get_num_configured_offsets(self.config) };
+        let mut offsets = vec![0; num_lines];
+
+        // SAFETY: gpiod_line_config is guaranteed to be valid here.
+        let num_stored = unsafe {
+            gpiod::gpiod_line_config_get_configured_offsets(
+                self.config,
+                offsets.as_mut_ptr(),
+                num_lines,
+            )
+        };
+
+        for offset in &offsets[0..num_stored] {
+            // SAFETY: `gpiod_line_config` is guaranteed to be valid here.
+            let settings =
+                unsafe { gpiod::gpiod_line_config_get_line_settings(self.config, *offset) };
+            if settings.is_null() {
+                return Err(Error::OperationFailed(
+                    OperationType::LineConfigGetSettings,
+                    errno::errno(),
+                ));
+            }
+
+            map.insert(*offset as u64, Settings::new_with_settings(settings));
+        }
+
+        Ok(map)
+    }
+}
+
+impl Drop for Config {
+    /// Free the line config object and release all associated resources.
+    fn drop(&mut self) {
+        // SAFETY: `gpiod_line_config` is guaranteed to be valid here.
+        unsafe { gpiod::gpiod_line_config_free(self.config) }
+    }
+}
diff --git a/bindings/rust/libgpiod/src/line_info.rs b/bindings/rust/libgpiod/src/line_info.rs
new file mode 100644 (file)
index 0000000..bd290f6
--- /dev/null
@@ -0,0 +1,226 @@
+// SPDX-License-Identifier: Apache-2.0 OR BSD-3-Clause
+// SPDX-FileCopyrightText: 2022 Linaro Ltd.
+// SPDX-FileCopyrightText: 2022 Viresh Kumar <viresh.kumar@linaro.org>
+
+use std::ops::Deref;
+use std::str;
+use std::time::Duration;
+use std::{ffi::CStr, marker::PhantomData};
+
+use super::{
+    gpiod,
+    line::{Bias, Direction, Drive, Edge, EventClock, Offset},
+    Error, Result,
+};
+
+/// Line info reference
+///
+/// Exposes functions for retrieving kernel information about both requested and
+/// free lines.  Line info object contains an immutable snapshot of a line's status.
+///
+/// The line info contains all the publicly available information about a
+/// line, which does not include the line value.  The line must be requested
+/// to access the line value.
+///
+/// [InfoRef] only abstracts a reference to a [gpiod::gpiod_line_info] instance whose lifetime is managed
+/// by a different object instance. The owned counter-part of this type is [Info].
+#[derive(Debug)]
+#[repr(transparent)]
+pub struct InfoRef {
+    _info: gpiod::gpiod_line_info,
+    // Avoid the automatic `Sync` implementation.
+    //
+    // The C lib does not allow parallel invocations of the API. We could model
+    // that by restricting all wrapper functions to `&mut Info` - which would
+    // ensure exclusive access. But that would make the API a bit weird...
+    // So instead we just suppress the `Sync` implementation, which suppresses
+    // the `Send` implementation on `&Info` - disallowing to send it to other
+    // threads, making concurrent use impossible.
+    _not_sync: PhantomData<*mut gpiod::gpiod_line_info>,
+}
+
+impl InfoRef {
+    /// Converts a non-owning pointer to a wrapper reference of a specific
+    /// lifetime
+    ///
+    /// No ownership will be assumed, the pointer must be free'd by the original
+    /// owner.
+    ///
+    /// SAFETY: The pointer must point to an instance that is valid for the
+    /// entire lifetime 'a. The instance must be owned by an object that is
+    /// owned by the thread invoking this method. The owning object may not be
+    /// moved to another thread for the entire lifetime 'a.
+    pub(crate) unsafe fn from_raw<'a>(info: *mut gpiod::gpiod_line_info) -> &'a InfoRef {
+        &*(info as *mut _)
+    }
+
+    fn as_raw_ptr(&self) -> *mut gpiod::gpiod_line_info {
+        self as *const _ as *mut _
+    }
+
+    /// Clones the line info object.
+    pub fn try_clone(&self) -> Result<Info> {
+        // SAFETY: `gpiod_line_info` is guaranteed to be valid here.
+        let copy = unsafe { gpiod::gpiod_line_info_copy(self.as_raw_ptr()) };
+        if copy.is_null() {
+            return Err(Error::OperationFailed(
+                crate::OperationType::LineInfoCopy,
+                errno::errno(),
+            ));
+        }
+
+        // SAFETY: The copy succeeded, we are the owner and stop using the
+        // pointer after this.
+        Ok(unsafe { Info::from_raw(copy) })
+    }
+
+    /// Get the offset of the line within the GPIO chip.
+    ///
+    /// The offset uniquely identifies the line on the chip. The combination of the chip and offset
+    /// uniquely identifies the line within the system.
+    pub fn offset(&self) -> Offset {
+        // SAFETY: `gpiod_line_info` is guaranteed to be valid here.
+        unsafe { gpiod::gpiod_line_info_get_offset(self.as_raw_ptr()) }
+    }
+
+    /// Get GPIO line's name.
+    pub fn name(&self) -> Result<&str> {
+        // SAFETY: The string returned by libgpiod is guaranteed to live as long
+        // as the `struct Info`.
+        let name = unsafe { gpiod::gpiod_line_info_get_name(self.as_raw_ptr()) };
+        if name.is_null() {
+            return Err(Error::NullString("GPIO line's name"));
+        }
+
+        // SAFETY: The string is guaranteed to be valid here by the C API.
+        unsafe { CStr::from_ptr(name) }
+            .to_str()
+            .map_err(Error::StringNotUtf8)
+    }
+
+    /// Returns True if the line is in use, false otherwise.
+    ///
+    /// The user space can't know exactly why a line is busy. It may have been
+    /// requested by another process or hogged by the kernel. It only matters that
+    /// the line is used and we can't request it.
+    pub fn is_used(&self) -> bool {
+        // SAFETY: `gpiod_line_info` is guaranteed to be valid here.
+        unsafe { gpiod::gpiod_line_info_is_used(self.as_raw_ptr()) }
+    }
+
+    /// Get the GPIO line's consumer name.
+    pub fn consumer(&self) -> Result<&str> {
+        // SAFETY: The string returned by libgpiod is guaranteed to live as long
+        // as the `struct Info`.
+        let name = unsafe { gpiod::gpiod_line_info_get_consumer(self.as_raw_ptr()) };
+        if name.is_null() {
+            return Err(Error::NullString("GPIO line's consumer name"));
+        }
+
+        // SAFETY: The string is guaranteed to be valid here by the C API.
+        unsafe { CStr::from_ptr(name) }
+            .to_str()
+            .map_err(Error::StringNotUtf8)
+    }
+
+    /// Get the GPIO line's direction.
+    pub fn direction(&self) -> Result<Direction> {
+        // SAFETY: `gpiod_line_info` is guaranteed to be valid here.
+        Direction::new(unsafe { gpiod::gpiod_line_info_get_direction(self.as_raw_ptr()) })
+    }
+
+    /// Returns true if the line is "active-low", false otherwise.
+    pub fn is_active_low(&self) -> bool {
+        // SAFETY: `gpiod_line_info` is guaranteed to be valid here.
+        unsafe { gpiod::gpiod_line_info_is_active_low(self.as_raw_ptr()) }
+    }
+
+    /// Get the GPIO line's bias setting.
+    pub fn bias(&self) -> Result<Option<Bias>> {
+        // SAFETY: `gpiod_line_info` is guaranteed to be valid here.
+        Bias::new(unsafe { gpiod::gpiod_line_info_get_bias(self.as_raw_ptr()) })
+    }
+
+    /// Get the GPIO line's drive setting.
+    pub fn drive(&self) -> Result<Drive> {
+        // SAFETY: `gpiod_line_info` is guaranteed to be valid here.
+        Drive::new(unsafe { gpiod::gpiod_line_info_get_drive(self.as_raw_ptr()) })
+    }
+
+    /// Get the current edge detection setting of the line.
+    pub fn edge_detection(&self) -> Result<Option<Edge>> {
+        // SAFETY: `gpiod_line_info` is guaranteed to be valid here.
+        Edge::new(unsafe { gpiod::gpiod_line_info_get_edge_detection(self.as_raw_ptr()) })
+    }
+
+    /// Get the current event clock setting used for edge event timestamps.
+    pub fn event_clock(&self) -> Result<EventClock> {
+        // SAFETY: `gpiod_line_info` is guaranteed to be valid here.
+        EventClock::new(unsafe { gpiod::gpiod_line_info_get_event_clock(self.as_raw_ptr()) })
+    }
+
+    /// Returns true if the line is debounced (either by hardware or by the
+    /// kernel software debouncer), false otherwise.
+    pub fn is_debounced(&self) -> bool {
+        // SAFETY: `gpiod_line_info` is guaranteed to be valid here.
+        unsafe { gpiod::gpiod_line_info_is_debounced(self.as_raw_ptr()) }
+    }
+
+    /// Get the debounce period of the line.
+    pub fn debounce_period(&self) -> Duration {
+        // c_ulong may be 32bit OR 64bit, clippy reports a false-positive here:
+        // https://github.com/rust-lang/rust-clippy/issues/10555
+        #[allow(clippy::unnecessary_cast)]
+        // SAFETY: `gpiod_line_info` is guaranteed to be valid here.
+        Duration::from_micros(unsafe {
+            gpiod::gpiod_line_info_get_debounce_period_us(self.as_raw_ptr()) as u64
+        })
+    }
+}
+
+/// Line info
+///
+/// This is the owned counterpart to [InfoRef]. Due to a [Deref] implementation,
+/// all functions of [InfoRef] can also be called on this type.
+#[derive(Debug)]
+pub struct Info {
+    info: *mut gpiod::gpiod_line_info,
+}
+
+// SAFETY: Info models a owned instance whose ownership may be safely
+// transferred to other threads.
+unsafe impl Send for Info {}
+
+impl Info {
+    /// Converts a owned pointer into an owned instance
+    ///
+    /// Assumes sole ownership over a [gpiod::gpiod_line_info] instance.
+    ///
+    /// SAFETY: The pointer must point to an instance that is valid. After
+    /// constructing an [Info] the pointer MUST NOT be used for any other
+    /// purpose anymore. All interactions with the libgpiod API have to happen
+    /// through this object.
+    pub(crate) unsafe fn from_raw(info: *mut gpiod::gpiod_line_info) -> Info {
+        Info { info }
+    }
+}
+
+impl Deref for Info {
+    type Target = InfoRef;
+
+    fn deref(&self) -> &Self::Target {
+        // SAFETY: The pointer is valid for the entire lifetime '0. Info is not
+        // Sync. Therefore, no &Info may be held by a different thread. Hence,
+        // the current thread owns it. Since we borrow with the lifetime of '0,
+        // no move to a different thread can occur while a reference remains
+        // being hold.
+        unsafe { InfoRef::from_raw(self.info) }
+    }
+}
+
+impl Drop for Info {
+    fn drop(&mut self) {
+        // SAFETY: `gpiod_line_info` is guaranteed to be valid here.
+        unsafe { gpiod::gpiod_line_info_free(self.info) }
+    }
+}
diff --git a/bindings/rust/libgpiod/src/line_request.rs b/bindings/rust/libgpiod/src/line_request.rs
new file mode 100644 (file)
index 0000000..a7fe6d0
--- /dev/null
@@ -0,0 +1,253 @@
+// SPDX-License-Identifier: Apache-2.0 OR BSD-3-Clause
+// SPDX-FileCopyrightText: 2022 Linaro Ltd.
+// SPDX-FileCopyrightText: 2022 Viresh Kumar <viresh.kumar@linaro.org>
+
+#[cfg(feature = "vnext")]
+use std::ffi::CStr;
+use std::os::unix::prelude::AsRawFd;
+use std::time::Duration;
+
+use super::{
+    gpiod,
+    line::{self, Offset, Value, ValueMap},
+    request, Error, OperationType, Result,
+};
+
+/// Line request operations
+///
+/// Allows interaction with a set of requested lines.
+#[derive(Debug, Eq, PartialEq)]
+pub struct Request {
+    pub(crate) request: *mut gpiod::gpiod_line_request,
+}
+
+// SAFETY: Request models a wrapper around an owned gpiod_line_request and may
+// be safely sent to other threads.
+unsafe impl Send for Request {}
+
+impl Request {
+    /// Request a set of lines for exclusive usage.
+    pub(crate) fn new(request: *mut gpiod::gpiod_line_request) -> Result<Self> {
+        Ok(Self { request })
+    }
+
+    /// Get the name of the chip this request was made on.
+    #[cfg(feature = "vnext")]
+    pub fn chip_name(&self) -> Result<&str> {
+        // SAFETY: The `gpiod_line_request` is guaranteed to be live as long
+        // as `&self`
+        let name = unsafe { gpiod::gpiod_line_request_get_chip_name(self.request) };
+
+        // SAFETY: The string is guaranteed to be valid, non-null and immutable
+        // by the C API for the lifetime of the `gpiod_line_request`.
+        unsafe { CStr::from_ptr(name) }
+            .to_str()
+            .map_err(Error::StringNotUtf8)
+    }
+
+    /// Get the number of lines in the request.
+    pub fn num_lines(&self) -> usize {
+        // SAFETY: `gpiod_line_request` is guaranteed to be valid here.
+        unsafe { gpiod::gpiod_line_request_get_num_requested_lines(self.request) }
+    }
+
+    /// Get the offsets of lines in the request.
+    pub fn offsets(&self) -> Vec<Offset> {
+        let mut offsets = vec![0; self.num_lines()];
+
+        // SAFETY: `gpiod_line_request` is guaranteed to be valid here.
+        let num_offsets = unsafe {
+            gpiod::gpiod_line_request_get_requested_offsets(
+                self.request,
+                offsets.as_mut_ptr(),
+                self.num_lines(),
+            )
+        };
+        offsets.shrink_to(num_offsets);
+        offsets
+    }
+
+    /// Get the value (0 or 1) of a single line associated with the request.
+    pub fn value(&self, offset: Offset) -> Result<Value> {
+        // SAFETY: `gpiod_line_request` is guaranteed to be valid here.
+        let value = unsafe { gpiod::gpiod_line_request_get_value(self.request, offset) };
+
+        if value != 0 && value != 1 {
+            Err(Error::OperationFailed(
+                OperationType::LineRequestGetVal,
+                errno::errno(),
+            ))
+        } else {
+            Value::new(value)
+        }
+    }
+
+    /// Get values of a subset of lines associated with the request.
+    pub fn values_subset(&self, offsets: &[Offset]) -> Result<ValueMap> {
+        let mut values = vec![0; offsets.len()];
+
+        // SAFETY: `gpiod_line_request` is guaranteed to be valid here.
+        let ret = unsafe {
+            gpiod::gpiod_line_request_get_values_subset(
+                self.request,
+                offsets.len(),
+                offsets.as_ptr(),
+                values.as_mut_ptr(),
+            )
+        };
+
+        if ret == -1 {
+            Err(Error::OperationFailed(
+                OperationType::LineRequestGetValSubset,
+                errno::errno(),
+            ))
+        } else {
+            let mut map = ValueMap::new();
+
+            for (i, val) in values.iter().enumerate() {
+                map.insert(offsets[i].into(), Value::new(*val)?);
+            }
+
+            Ok(map)
+        }
+    }
+
+    /// Get values of all lines associated with the request.
+    pub fn values(&self) -> Result<ValueMap> {
+        self.values_subset(&self.offsets())
+    }
+
+    /// Set the value of a single line associated with the request.
+    pub fn set_value(&mut self, offset: Offset, value: Value) -> Result<&mut Self> {
+        // SAFETY: `gpiod_line_request` is guaranteed to be valid here.
+        let ret =
+            unsafe { gpiod::gpiod_line_request_set_value(self.request, offset, value.value()) };
+
+        if ret == -1 {
+            Err(Error::OperationFailed(
+                OperationType::LineRequestSetVal,
+                errno::errno(),
+            ))
+        } else {
+            Ok(self)
+        }
+    }
+
+    /// Set values of a subset of lines associated with the request.
+    pub fn set_values_subset(&mut self, map: ValueMap) -> Result<&mut Self> {
+        let mut offsets = Vec::new();
+        let mut values = Vec::new();
+
+        for (offset, value) in map {
+            offsets.push(offset as u32);
+            values.push(value.value());
+        }
+
+        // SAFETY: `gpiod_line_request` is guaranteed to be valid here.
+        let ret = unsafe {
+            gpiod::gpiod_line_request_set_values_subset(
+                self.request,
+                offsets.len(),
+                offsets.as_ptr(),
+                values.as_ptr(),
+            )
+        };
+
+        if ret == -1 {
+            Err(Error::OperationFailed(
+                OperationType::LineRequestSetValSubset,
+                errno::errno(),
+            ))
+        } else {
+            Ok(self)
+        }
+    }
+
+    /// Set values of all lines associated with the request.
+    pub fn set_values(&mut self, values: &[Value]) -> Result<&mut Self> {
+        if values.len() != self.num_lines() {
+            return Err(Error::InvalidArguments);
+        }
+
+        let mut new_values = Vec::new();
+        for value in values {
+            new_values.push(value.value());
+        }
+
+        // SAFETY: `gpiod_line_request` is guaranteed to be valid here.
+        let ret =
+            unsafe { gpiod::gpiod_line_request_set_values(self.request, new_values.as_ptr()) };
+
+        if ret == -1 {
+            Err(Error::OperationFailed(
+                OperationType::LineRequestSetVal,
+                errno::errno(),
+            ))
+        } else {
+            Ok(self)
+        }
+    }
+
+    /// Update the configuration of lines associated with the line request.
+    pub fn reconfigure_lines(&mut self, lconfig: &line::Config) -> Result<&mut Self> {
+        // SAFETY: `gpiod_line_request` is guaranteed to be valid here.
+        let ret =
+            unsafe { gpiod::gpiod_line_request_reconfigure_lines(self.request, lconfig.config) };
+
+        if ret == -1 {
+            Err(Error::OperationFailed(
+                OperationType::LineRequestReconfigLines,
+                errno::errno(),
+            ))
+        } else {
+            Ok(self)
+        }
+    }
+
+    /// Wait for edge events on any of the lines associated with the request.
+    pub fn wait_edge_events(&self, timeout: Option<Duration>) -> Result<bool> {
+        let timeout = match timeout {
+            Some(x) => x.as_nanos() as i64,
+            // Block indefinitely
+            None => -1,
+        };
+
+        // SAFETY: `gpiod_line_request` is guaranteed to be valid here.
+        let ret = unsafe { gpiod::gpiod_line_request_wait_edge_events(self.request, timeout) };
+
+        match ret {
+            -1 => Err(Error::OperationFailed(
+                OperationType::LineRequestWaitEdgeEvent,
+                errno::errno(),
+            )),
+            0 => Ok(false),
+            _ => Ok(true),
+        }
+    }
+
+    /// Get a number of edge events from a line request.
+    ///
+    /// This function will block if no event was queued for the line.
+    pub fn read_edge_events<'a>(
+        &'a self,
+        buffer: &'a mut request::Buffer,
+    ) -> Result<request::Events> {
+        buffer.read_edge_events(self)
+    }
+}
+
+impl AsRawFd for Request {
+    /// Get the file descriptor associated with the line request.
+    fn as_raw_fd(&self) -> i32 {
+        // SAFETY: `gpiod_line_request` is guaranteed to be valid here.
+        unsafe { gpiod::gpiod_line_request_get_fd(self.request) }
+    }
+}
+
+impl Drop for Request {
+    /// Release the requested lines and free all associated resources.
+    fn drop(&mut self) {
+        // SAFETY: `gpiod_line_request` is guaranteed to be valid here.
+        unsafe { gpiod::gpiod_line_request_release(self.request) }
+    }
+}
diff --git a/bindings/rust/libgpiod/src/line_settings.rs b/bindings/rust/libgpiod/src/line_settings.rs
new file mode 100644 (file)
index 0000000..4ba20d4
--- /dev/null
@@ -0,0 +1,295 @@
+// SPDX-License-Identifier: Apache-2.0 OR BSD-3-Clause
+// SPDX-FileCopyrightText: 2022 Linaro Ltd.
+// SPDX-FileCopyrightText: 2022 Viresh Kumar <viresh.kumar@linaro.org>
+
+use std::time::Duration;
+
+use super::{
+    gpiod,
+    line::{Bias, Direction, Drive, Edge, EventClock, SettingKind, SettingVal, Value},
+    Error, OperationType, Result,
+};
+
+/// Line settings objects.
+///
+/// Line settings object contains a set of line properties that can be used
+/// when requesting lines or reconfiguring an existing request.
+///
+/// Mutators in general can only fail if the new property value is invalid. The
+/// return values can be safely ignored - the object remains valid even after
+/// a mutator fails and simply uses the sane default appropriate for given
+/// property.
+
+#[derive(Debug, Eq, PartialEq)]
+pub struct Settings {
+    pub(crate) settings: *mut gpiod::gpiod_line_settings,
+}
+
+// SAFETY: Settings models a wrapper around an owned gpiod_line_settings and may
+// be safely sent to other threads.
+unsafe impl Send for Settings {}
+
+impl Settings {
+    /// Create a new line settings object.
+    pub fn new() -> Result<Self> {
+        // SAFETY: The `gpiod_line_settings` returned by libgpiod is guaranteed to live as long
+        // as the `struct Settings`.
+        let settings = unsafe { gpiod::gpiod_line_settings_new() };
+
+        if settings.is_null() {
+            return Err(Error::OperationFailed(
+                OperationType::LineSettingsNew,
+                errno::errno(),
+            ));
+        }
+
+        Ok(Self { settings })
+    }
+
+    pub fn new_with_settings(settings: *mut gpiod::gpiod_line_settings) -> Self {
+        Self { settings }
+    }
+
+    /// Resets the line settings object to its default values.
+    pub fn reset(&mut self) {
+        // SAFETY: `gpiod_line_settings` is guaranteed to be valid here.
+        unsafe { gpiod::gpiod_line_settings_reset(self.settings) }
+    }
+
+    /// Makes a copy of the settings object.
+    pub fn try_clone(&self) -> Result<Self> {
+        // SAFETY: `gpiod_line_settings` is guaranteed to be valid here.
+        let settings = unsafe { gpiod::gpiod_line_settings_copy(self.settings) };
+        if settings.is_null() {
+            return Err(Error::OperationFailed(
+                OperationType::LineSettingsCopy,
+                errno::errno(),
+            ));
+        }
+
+        Ok(Self { settings })
+    }
+
+    /// Set line prop setting.
+    pub fn set_prop(&mut self, props: &[SettingVal]) -> Result<&mut Self> {
+        for property in props {
+            match property {
+                SettingVal::Direction(prop) => self.set_direction(*prop)?,
+                SettingVal::EdgeDetection(prop) => self.set_edge_detection(*prop)?,
+                SettingVal::Bias(prop) => self.set_bias(*prop)?,
+                SettingVal::Drive(prop) => self.set_drive(*prop)?,
+                SettingVal::ActiveLow(prop) => self.set_active_low(*prop),
+                SettingVal::DebouncePeriod(prop) => self.set_debounce_period(*prop),
+                SettingVal::EventClock(prop) => self.set_event_clock(*prop)?,
+                SettingVal::OutputValue(prop) => self.set_output_value(*prop)?,
+            };
+        }
+
+        Ok(self)
+    }
+
+    /// Get the line prop setting.
+    pub fn prop(&self, property: SettingKind) -> Result<SettingVal> {
+        Ok(match property {
+            SettingKind::Direction => SettingVal::Direction(self.direction()?),
+            SettingKind::EdgeDetection => SettingVal::EdgeDetection(self.edge_detection()?),
+            SettingKind::Bias => SettingVal::Bias(self.bias()?),
+            SettingKind::Drive => SettingVal::Drive(self.drive()?),
+            SettingKind::ActiveLow => SettingVal::ActiveLow(self.active_low()),
+            SettingKind::DebouncePeriod => SettingVal::DebouncePeriod(self.debounce_period()?),
+            SettingKind::EventClock => SettingVal::EventClock(self.event_clock()?),
+            SettingKind::OutputValue => SettingVal::OutputValue(self.output_value()?),
+        })
+    }
+
+    /// Set the line direction.
+    pub fn set_direction(&mut self, direction: Direction) -> Result<&mut Self> {
+        // SAFETY: `gpiod_line_settings` is guaranteed to be valid here.
+        let ret = unsafe {
+            gpiod::gpiod_line_settings_set_direction(self.settings, direction.gpiod_direction())
+        };
+
+        if ret == -1 {
+            Err(Error::OperationFailed(
+                OperationType::LineSettingsSetDirection,
+                errno::errno(),
+            ))
+        } else {
+            Ok(self)
+        }
+    }
+
+    /// Get the direction setting.
+    pub fn direction(&self) -> Result<Direction> {
+        // SAFETY: `gpiod_line_settings` is guaranteed to be valid here.
+        Direction::new(unsafe { gpiod::gpiod_line_settings_get_direction(self.settings) })
+    }
+
+    /// Set the edge event detection setting.
+    pub fn set_edge_detection(&mut self, edge: Option<Edge>) -> Result<&mut Self> {
+        // SAFETY: `gpiod_line_settings` is guaranteed to be valid here.
+        let ret = unsafe {
+            gpiod::gpiod_line_settings_set_edge_detection(self.settings, Edge::gpiod_edge(edge))
+        };
+
+        if ret == -1 {
+            Err(Error::OperationFailed(
+                OperationType::LineSettingsSetEdgeDetection,
+                errno::errno(),
+            ))
+        } else {
+            Ok(self)
+        }
+    }
+
+    /// Get the edge event detection setting.
+    pub fn edge_detection(&self) -> Result<Option<Edge>> {
+        // SAFETY: `gpiod_line_settings` is guaranteed to be valid here.
+        Edge::new(unsafe { gpiod::gpiod_line_settings_get_edge_detection(self.settings) })
+    }
+
+    /// Set the bias setting.
+    pub fn set_bias(&mut self, bias: Option<Bias>) -> Result<&mut Self> {
+        // SAFETY: `gpiod_line_settings` is guaranteed to be valid here.
+        let ret =
+            unsafe { gpiod::gpiod_line_settings_set_bias(self.settings, Bias::gpiod_bias(bias)) };
+
+        if ret == -1 {
+            Err(Error::OperationFailed(
+                OperationType::LineSettingsSetBias,
+                errno::errno(),
+            ))
+        } else {
+            Ok(self)
+        }
+    }
+
+    /// Get the bias setting.
+    pub fn bias(&self) -> Result<Option<Bias>> {
+        // SAFETY: `gpiod_line_settings` is guaranteed to be valid here.
+        Bias::new(unsafe { gpiod::gpiod_line_settings_get_bias(self.settings) })
+    }
+
+    /// Set the drive setting.
+    pub fn set_drive(&mut self, drive: Drive) -> Result<&mut Self> {
+        // SAFETY: `gpiod_line_settings` is guaranteed to be valid here.
+        let ret =
+            unsafe { gpiod::gpiod_line_settings_set_drive(self.settings, drive.gpiod_drive()) };
+
+        if ret == -1 {
+            Err(Error::OperationFailed(
+                OperationType::LineSettingsSetDrive,
+                errno::errno(),
+            ))
+        } else {
+            Ok(self)
+        }
+    }
+
+    /// Get the drive setting.
+    pub fn drive(&self) -> Result<Drive> {
+        // SAFETY: `gpiod_line_settings` is guaranteed to be valid here.
+        Drive::new(unsafe { gpiod::gpiod_line_settings_get_drive(self.settings) })
+    }
+
+    /// Set active-low setting.
+    pub fn set_active_low(&mut self, active_low: bool) -> &mut Self {
+        // SAFETY: `gpiod_line_settings` is guaranteed to be valid here.
+        unsafe {
+            gpiod::gpiod_line_settings_set_active_low(self.settings, active_low);
+        }
+        self
+    }
+
+    /// Check the active-low setting.
+    pub fn active_low(&self) -> bool {
+        // SAFETY: `gpiod_line_settings` is guaranteed to be valid here.
+        unsafe { gpiod::gpiod_line_settings_get_active_low(self.settings) }
+    }
+
+    /// Set the debounce period setting.
+    pub fn set_debounce_period(&mut self, period: Duration) -> &mut Self {
+        // SAFETY: `gpiod_line_settings` is guaranteed to be valid here.
+        unsafe {
+            gpiod::gpiod_line_settings_set_debounce_period_us(
+                self.settings,
+                period.as_micros().try_into().unwrap(),
+            );
+        }
+
+        self
+    }
+
+    /// Get the debounce period.
+    pub fn debounce_period(&self) -> Result<Duration> {
+        // c_ulong may be 32bit OR 64bit, clippy reports a false-positive here:
+        // https://github.com/rust-lang/rust-clippy/issues/10555
+        #[allow(clippy::unnecessary_cast)]
+        // SAFETY: `gpiod_line_settings` is guaranteed to be valid here.
+        Ok(Duration::from_micros(unsafe {
+            gpiod::gpiod_line_settings_get_debounce_period_us(self.settings) as u64
+        }))
+    }
+
+    /// Set the event clock setting.
+    pub fn set_event_clock(&mut self, clock: EventClock) -> Result<&mut Self> {
+        // SAFETY: `gpiod_line_settings` is guaranteed to be valid here.
+        let ret = unsafe {
+            gpiod::gpiod_line_settings_set_event_clock(self.settings, clock.gpiod_clock())
+        };
+
+        if ret == -1 {
+            Err(Error::OperationFailed(
+                OperationType::LineSettingsSetEventClock,
+                errno::errno(),
+            ))
+        } else {
+            Ok(self)
+        }
+    }
+
+    /// Get the event clock setting.
+    pub fn event_clock(&self) -> Result<EventClock> {
+        // SAFETY: `gpiod_line_settings` is guaranteed to be valid here.
+        EventClock::new(unsafe { gpiod::gpiod_line_settings_get_event_clock(self.settings) })
+    }
+
+    /// Set the output value setting.
+    pub fn set_output_value(&mut self, value: Value) -> Result<&mut Self> {
+        // SAFETY: `gpiod_line_settings` is guaranteed to be valid here.
+        let ret =
+            unsafe { gpiod::gpiod_line_settings_set_output_value(self.settings, value.value()) };
+
+        if ret == -1 {
+            Err(Error::OperationFailed(
+                OperationType::LineSettingsSetOutputValue,
+                errno::errno(),
+            ))
+        } else {
+            Ok(self)
+        }
+    }
+
+    /// Get the output value, 0 or 1.
+    pub fn output_value(&self) -> Result<Value> {
+        // SAFETY: `gpiod_line_settings` is guaranteed to be valid here.
+        let value = unsafe { gpiod::gpiod_line_settings_get_output_value(self.settings) };
+
+        if value != 0 && value != 1 {
+            Err(Error::OperationFailed(
+                OperationType::LineSettingsGetOutVal,
+                errno::errno(),
+            ))
+        } else {
+            Value::new(value)
+        }
+    }
+}
+
+impl Drop for Settings {
+    /// Free the line settings object and release all associated resources.
+    fn drop(&mut self) {
+        // SAFETY: `gpiod_line_settings` is guaranteed to be valid here.
+        unsafe { gpiod::gpiod_line_settings_free(self.settings) }
+    }
+}
diff --git a/bindings/rust/libgpiod/src/request_config.rs b/bindings/rust/libgpiod/src/request_config.rs
new file mode 100644 (file)
index 0000000..9b66cc9
--- /dev/null
@@ -0,0 +1,100 @@
+// SPDX-License-Identifier: Apache-2.0 OR BSD-3-Clause
+// SPDX-FileCopyrightText: 2022 Linaro Ltd.
+// SPDX-FileCopyrightText: 2022 Viresh Kumar <viresh.kumar@linaro.org>
+
+use std::ffi::{CStr, CString};
+use std::os::raw::c_char;
+use std::str;
+
+use super::{gpiod, Error, OperationType, Result};
+
+/// Request configuration objects
+///
+/// Request config objects are used to pass a set of options to the kernel at
+/// the time of the line request. The mutators don't return error values. If the
+/// values are invalid, in general they are silently adjusted to acceptable
+/// ranges.
+
+#[derive(Debug, Eq, PartialEq)]
+pub struct Config {
+    pub(crate) config: *mut gpiod::gpiod_request_config,
+}
+
+// SAFETY: Config models a wrapper around an owned gpiod_request_config and may
+// be safely sent to other threads.
+unsafe impl Send for Config {}
+
+impl Config {
+    /// Create a new request config object.
+    pub fn new() -> Result<Self> {
+        // SAFETY: The `gpiod_request_config` returned by libgpiod is guaranteed to live as long
+        // as the `struct Config`.
+        let config = unsafe { gpiod::gpiod_request_config_new() };
+        if config.is_null() {
+            return Err(Error::OperationFailed(
+                OperationType::RequestConfigNew,
+                errno::errno(),
+            ));
+        }
+
+        Ok(Self { config })
+    }
+
+    /// Set the consumer name for the request.
+    ///
+    /// If the consumer string is too long, it will be truncated to the max
+    /// accepted length.
+    pub fn set_consumer(&mut self, consumer: &str) -> Result<&mut Self> {
+        let consumer = CString::new(consumer).map_err(|_| Error::InvalidString)?;
+
+        // SAFETY: `gpiod_request_config` is guaranteed to be valid here.
+        unsafe {
+            gpiod::gpiod_request_config_set_consumer(
+                self.config,
+                consumer.as_ptr() as *const c_char,
+            )
+        }
+
+        Ok(self)
+    }
+
+    /// Get the consumer name configured in the request config.
+    pub fn consumer(&self) -> Result<&str> {
+        // SAFETY: The string returned by libgpiod is guaranteed to live as long
+        // as the `struct Config`.
+        let consumer = unsafe { gpiod::gpiod_request_config_get_consumer(self.config) };
+        if consumer.is_null() {
+            return Err(Error::OperationFailed(
+                OperationType::RequestConfigGetConsumer,
+                errno::errno(),
+            ));
+        }
+
+        // SAFETY: The string is guaranteed to be valid here by the C API.
+        unsafe { CStr::from_ptr(consumer) }
+            .to_str()
+            .map_err(Error::StringNotUtf8)
+    }
+
+    /// Set the size of the kernel event buffer for the request.
+    pub fn set_event_buffer_size(&mut self, size: usize) -> &mut Self {
+        // SAFETY: `gpiod_request_config` is guaranteed to be valid here.
+        unsafe { gpiod::gpiod_request_config_set_event_buffer_size(self.config, size) }
+
+        self
+    }
+
+    /// Get the edge event buffer size setting for the request config.
+    pub fn event_buffer_size(&self) -> usize {
+        // SAFETY: `gpiod_request_config` is guaranteed to be valid here.
+        unsafe { gpiod::gpiod_request_config_get_event_buffer_size(self.config) }
+    }
+}
+
+impl Drop for Config {
+    /// Free the request config object and release all associated resources.
+    fn drop(&mut self) {
+        // SAFETY: `gpiod_request_config` is guaranteed to be valid here.
+        unsafe { gpiod::gpiod_request_config_free(self.config) }
+    }
+}
diff --git a/bindings/rust/libgpiod/tests/Makefile.am b/bindings/rust/libgpiod/tests/Makefile.am
new file mode 100644 (file)
index 0000000..8927649
--- /dev/null
@@ -0,0 +1,15 @@
+# SPDX-License-Identifier: GPL-2.0-or-later
+# SPDX-FileCopyrightText: 2022 Linaro Ltd.
+# SPDX-FileCopyrightText: 2022 Bartosz Golaszewski <bartosz.golaszewski@linaro.org>
+
+EXTRA_DIST = \
+       chip.rs \
+       edge_event.rs \
+       info_event.rs \
+       line_config.rs \
+       line_info.rs \
+       line_request.rs \
+       line_settings.rs \
+       request_config.rs
+
+SUBDIRS = common
diff --git a/bindings/rust/libgpiod/tests/chip.rs b/bindings/rust/libgpiod/tests/chip.rs
new file mode 100644 (file)
index 0000000..60b4ecc
--- /dev/null
@@ -0,0 +1,97 @@
+// SPDX-License-Identifier: Apache-2.0 OR BSD-3-Clause
+// SPDX-FileCopyrightText: 2022 Linaro Ltd.
+// SPDX-FileCopyrightText: 2022 Viresh Kumar <viresh.kumar@linaro.org>
+
+mod common;
+
+mod chip {
+    use libc::{ENODEV, ENOENT, ENOTTY};
+    use std::path::PathBuf;
+
+    use gpiosim_sys::Sim;
+    use libgpiod::{chip::Chip, Error as ChipError, OperationType};
+
+    mod open {
+        use super::*;
+
+        #[test]
+        fn nonexistent_file() {
+            assert_eq!(
+                Chip::open(&PathBuf::from("/dev/nonexistent")).unwrap_err(),
+                ChipError::OperationFailed(OperationType::ChipOpen, errno::Errno(ENOENT))
+            );
+        }
+
+        #[test]
+        fn no_dev_file() {
+            assert_eq!(
+                Chip::open(&PathBuf::from("/tmp")).unwrap_err(),
+                ChipError::OperationFailed(OperationType::ChipOpen, errno::Errno(ENOTTY))
+            );
+        }
+
+        #[test]
+        fn non_gpio_char_dev_file() {
+            assert_eq!(
+                Chip::open(&PathBuf::from("/dev/null")).unwrap_err(),
+                ChipError::OperationFailed(OperationType::ChipOpen, errno::Errno(ENODEV))
+            );
+        }
+
+        #[test]
+        fn gpiosim_file() {
+            let sim = Sim::new(None, None, true).unwrap();
+            assert!(Chip::open(&sim.dev_path()).is_ok());
+        }
+    }
+
+    mod verify {
+        use super::*;
+        const NGPIO: usize = 16;
+        const LABEL: &str = "foobar";
+
+        #[test]
+        fn basic_helpers() {
+            let sim = Sim::new(Some(NGPIO), Some(LABEL), true).unwrap();
+            let chip = Chip::open(&sim.dev_path()).unwrap();
+            let info = chip.info().unwrap();
+
+            assert_eq!(info.label().unwrap(), LABEL);
+            assert_eq!(info.name().unwrap(), sim.chip_name());
+            assert_eq!(chip.path().unwrap(), sim.dev_path().to_str().unwrap());
+            assert_eq!(info.num_lines(), NGPIO);
+        }
+
+        #[test]
+        fn find_line() {
+            let sim = Sim::new(Some(NGPIO), None, false).unwrap();
+            sim.set_line_name(0, "zero").unwrap();
+            sim.set_line_name(2, "two").unwrap();
+            sim.set_line_name(3, "three").unwrap();
+            sim.set_line_name(5, "five").unwrap();
+            sim.set_line_name(10, "ten").unwrap();
+            sim.set_line_name(11, "ten").unwrap();
+            sim.enable().unwrap();
+
+            let chip = Chip::open(&sim.dev_path()).unwrap();
+
+            // Success case
+            assert_eq!(chip.line_offset_from_name("zero").unwrap(), 0);
+            assert_eq!(chip.line_offset_from_name("two").unwrap(), 2);
+            assert_eq!(chip.line_offset_from_name("three").unwrap(), 3);
+            assert_eq!(chip.line_offset_from_name("five").unwrap(), 5);
+
+            // Success with duplicate names, should return first entry
+            assert_eq!(chip.line_offset_from_name("ten").unwrap(), 10);
+
+            // Failure
+            assert_eq!(
+                chip.line_offset_from_name("nonexistent").unwrap_err(),
+                ChipError::OperationFailed(
+                    OperationType::ChipGetLineOffsetFromName,
+                    errno::Errno(ENOENT),
+                )
+            );
+        }
+    }
+}
diff --git a/bindings/rust/libgpiod/tests/common/Makefile.am b/bindings/rust/libgpiod/tests/common/Makefile.am
new file mode 100644 (file)
index 0000000..6a32db4
--- /dev/null
@@ -0,0 +1,5 @@
+# SPDX-License-Identifier: GPL-2.0-or-later
+# SPDX-FileCopyrightText: 2022 Linaro Ltd.
+# SPDX-FileCopyrightText: 2022 Bartosz Golaszewski <bartosz.golaszewski@linaro.org>
+
+EXTRA_DIST = config.rs mod.rs
diff --git a/bindings/rust/libgpiod/tests/common/config.rs b/bindings/rust/libgpiod/tests/common/config.rs
new file mode 100644 (file)
index 0000000..7bb1f65
--- /dev/null
@@ -0,0 +1,142 @@
+// SPDX-License-Identifier: Apache-2.0 OR BSD-3-Clause
+// SPDX-FileCopyrightText: 2022 Linaro Ltd.
+// SPDX-FileCopyrightText: 2022 Viresh Kumar <viresh.kumar@linaro.org>
+
+use std::sync::{Arc, Mutex};
+use std::time::Duration;
+
+use gpiosim_sys::{Pull, Sim, Value as SimValue};
+use libgpiod::{
+    chip::Chip,
+    line::{self, Bias, Direction, Drive, Edge, EventClock, Offset, SettingVal, Value},
+    request, Result,
+};
+
+pub(crate) struct TestConfig {
+    sim: Arc<Mutex<Sim>>,
+    chip: Option<Chip>,
+    request: Option<request::Request>,
+    rconfig: request::Config,
+    lconfig: line::Config,
+    lsettings: Option<line::Settings>,
+}
+
+impl TestConfig {
+    pub(crate) fn new(ngpio: usize) -> Result<Self> {
+        Ok(Self {
+            sim: Arc::new(Mutex::new(Sim::new(Some(ngpio), None, true)?)),
+            chip: None,
+            request: None,
+            rconfig: request::Config::new().unwrap(),
+            lconfig: line::Config::new().unwrap(),
+            lsettings: Some(line::Settings::new().unwrap()),
+        })
+    }
+
+    pub(crate) fn set_pull(&self, offsets: &[Offset], pulls: &[Pull]) {
+        for i in 0..pulls.len() {
+            self.sim
+                .lock()
+                .unwrap()
+                .set_pull(offsets[i], pulls[i])
+                .unwrap();
+        }
+    }
+
+    pub(crate) fn rconfig_set_consumer(&mut self, consumer: &str) {
+        self.rconfig.set_consumer(consumer).unwrap();
+    }
+
+    pub(crate) fn lconfig_val(&mut self, dir: Option<Direction>, val: Option<Value>) {
+        let mut settings = Vec::new();
+
+        if let Some(dir) = dir {
+            settings.push(SettingVal::Direction(dir));
+        }
+
+        if let Some(val) = val {
+            settings.push(SettingVal::OutputValue(val));
+        }
+
+        if !settings.is_empty() {
+            self.lsettings().set_prop(&settings).unwrap();
+        }
+    }
+
+    pub(crate) fn lconfig_bias(&mut self, dir: Direction, bias: Option<Bias>) {
+        let settings = vec![SettingVal::Direction(dir), SettingVal::Bias(bias)];
+        self.lsettings().set_prop(&settings).unwrap();
+    }
+
+    pub(crate) fn lconfig_clock(&mut self, clock: EventClock) {
+        let settings = vec![SettingVal::EventClock(clock)];
+        self.lsettings().set_prop(&settings).unwrap();
+    }
+
+    pub(crate) fn lconfig_debounce(&mut self, duration: Duration) {
+        let settings = vec![
+            SettingVal::Direction(Direction::Input),
+            SettingVal::DebouncePeriod(duration),
+        ];
+        self.lsettings().set_prop(&settings).unwrap();
+    }
+
+    pub(crate) fn lconfig_drive(&mut self, dir: Direction, drive: Drive) {
+        let settings = vec![SettingVal::Direction(dir), SettingVal::Drive(drive)];
+        self.lsettings().set_prop(&settings).unwrap();
+    }
+
+    pub(crate) fn lconfig_edge(&mut self, dir: Option<Direction>, edge: Option<Edge>) {
+        let mut settings = Vec::new();
+
+        if let Some(dir) = dir {
+            settings.push(SettingVal::Direction(dir));
+        }
+
+        settings.push(SettingVal::EdgeDetection(edge));
+        self.lsettings().set_prop(&settings).unwrap();
+    }
+
+    pub(crate) fn lconfig_add_settings(&mut self, offsets: &[Offset]) {
+        self.lconfig
+            .add_line_settings(offsets, self.lsettings.take().unwrap())
+            .unwrap();
+    }
+
+    pub(crate) fn request_lines(&mut self) -> Result<()> {
+        let chip = Chip::open(&self.sim.lock().unwrap().dev_path())?;
+
+        self.request = Some(chip.request_lines(Some(&self.rconfig), &self.lconfig)?);
+        self.chip = Some(chip);
+
+        Ok(())
+    }
+
+    pub(crate) fn sim(&self) -> Arc<Mutex<Sim>> {
+        self.sim.clone()
+    }
+
+    pub(crate) fn sim_val(&self, offset: Offset) -> Result<SimValue> {
+        self.sim.lock().unwrap().val(offset)
+    }
+
+    pub(crate) fn chip(&self) -> &Chip {
+        self.chip.as_ref().unwrap()
+    }
+
+    pub(crate) fn lsettings(&mut self) -> &mut line::Settings {
+        self.lsettings.as_mut().unwrap()
+    }
+
+    pub(crate) fn request(&mut self) -> &mut request::Request {
+        self.request.as_mut().unwrap()
+    }
+}
+
+impl Drop for TestConfig {
+    fn drop(&mut self) {
+        // Explicit freeing is important to make sure "request" get freed
+        // before "sim" and "chip".
+        self.request = None;
+    }
+}
diff --git a/bindings/rust/libgpiod/tests/common/mod.rs b/bindings/rust/libgpiod/tests/common/mod.rs
new file mode 100644 (file)
index 0000000..586115b
--- /dev/null
@@ -0,0 +1,9 @@
+// SPDX-License-Identifier: Apache-2.0 OR BSD-3-Clause
+// SPDX-FileCopyrightText: 2022 Linaro Ltd.
+// SPDX-FileCopyrightText: 2022 Viresh Kumar <viresh.kumar@linaro.org>
+
+#[allow(dead_code)]
+mod config;
+
+#[allow(unused_imports)]
+pub(crate) use config::*;
diff --git a/bindings/rust/libgpiod/tests/edge_event.rs b/bindings/rust/libgpiod/tests/edge_event.rs
new file mode 100644 (file)
index 0000000..03b7e7c
--- /dev/null
@@ -0,0 +1,297 @@
+// SPDX-License-Identifier: Apache-2.0 OR BSD-3-Clause
+// SPDX-FileCopyrightText: 2022 Linaro Ltd.
+// SPDX-FileCopyrightText: 2022 Viresh Kumar <viresh.kumar@linaro.org>
+
+mod common;
+
+mod edge_event {
+    use std::time::Duration;
+
+    use crate::common::*;
+    use gpiosim_sys::{Pull, Sim};
+    use libgpiod::{
+        line::{Edge, EdgeKind, Offset},
+        request,
+    };
+
+    const NGPIO: usize = 8;
+
+    mod buffer_capacity {
+        use super::*;
+
+        #[test]
+        fn default_capacity() {
+            assert_eq!(request::Buffer::new(0).unwrap().capacity(), 64);
+        }
+
+        #[test]
+        fn user_defined_capacity() {
+            assert_eq!(request::Buffer::new(123).unwrap().capacity(), 123);
+        }
+
+        #[test]
+        fn max_capacity() {
+            assert_eq!(request::Buffer::new(1024 * 2).unwrap().capacity(), 1024);
+        }
+    }
+
+    mod trigger {
+        use super::*;
+        use std::{
+            sync::{Arc, Mutex},
+            thread,
+        };
+
+        // Helpers to generate events
+        fn trigger_falling_and_rising_edge(sim: Arc<Mutex<Sim>>, offset: Offset) {
+            thread::spawn(move || {
+                thread::sleep(Duration::from_millis(30));
+                sim.lock().unwrap().set_pull(offset, Pull::Up).unwrap();
+
+                thread::sleep(Duration::from_millis(30));
+                sim.lock().unwrap().set_pull(offset, Pull::Down).unwrap();
+            });
+        }
+
+        fn trigger_rising_edge_events_on_two_offsets(sim: Arc<Mutex<Sim>>, offset: [Offset; 2]) {
+            thread::spawn(move || {
+                thread::sleep(Duration::from_millis(30));
+                sim.lock().unwrap().set_pull(offset[0], Pull::Up).unwrap();
+
+                thread::sleep(Duration::from_millis(30));
+                sim.lock().unwrap().set_pull(offset[1], Pull::Up).unwrap();
+            });
+        }
+
+        fn trigger_multiple_events(sim: Arc<Mutex<Sim>>, offset: Offset) {
+            sim.lock().unwrap().set_pull(offset, Pull::Up).unwrap();
+            thread::sleep(Duration::from_millis(10));
+
+            sim.lock().unwrap().set_pull(offset, Pull::Down).unwrap();
+            thread::sleep(Duration::from_millis(10));
+
+            sim.lock().unwrap().set_pull(offset, Pull::Up).unwrap();
+            thread::sleep(Duration::from_millis(10));
+        }
+
+        #[test]
+        fn both_edges() {
+            const GPIO: Offset = 2;
+            let mut buf = request::Buffer::new(0).unwrap();
+            let mut config = TestConfig::new(NGPIO).unwrap();
+            config.lconfig_edge(None, Some(Edge::Both));
+            config.lconfig_add_settings(&[GPIO]);
+            config.request_lines().unwrap();
+
+            // Generate events
+            trigger_falling_and_rising_edge(config.sim(), GPIO);
+
+            // Rising event
+            assert!(config
+                .request()
+                .wait_edge_events(Some(Duration::from_secs(1)))
+                .unwrap());
+
+            let mut events = config.request().read_edge_events(&mut buf).unwrap();
+            assert_eq!(events.len(), 1);
+
+            let event = events.next().unwrap().unwrap();
+            let ts_rising = event.timestamp();
+            assert_eq!(event.event_type().unwrap(), EdgeKind::Rising);
+            assert_eq!(event.line_offset(), GPIO);
+
+            // Falling event
+            assert!(config
+                .request()
+                .wait_edge_events(Some(Duration::from_secs(1)))
+                .unwrap());
+
+            let mut events = config.request().read_edge_events(&mut buf).unwrap();
+            assert_eq!(events.len(), 1);
+
+            let event = events.next().unwrap().unwrap();
+            let ts_falling = event.timestamp();
+            assert_eq!(event.event_type().unwrap(), EdgeKind::Falling);
+            assert_eq!(event.line_offset(), GPIO);
+
+            // No events available
+            assert!(!config
+                .request()
+                .wait_edge_events(Some(Duration::from_millis(100)))
+                .unwrap());
+
+            assert!(ts_falling > ts_rising);
+        }
+
+        #[test]
+        fn rising_edge() {
+            const GPIO: Offset = 6;
+            let mut buf = request::Buffer::new(0).unwrap();
+            let mut config = TestConfig::new(NGPIO).unwrap();
+            config.lconfig_edge(None, Some(Edge::Rising));
+            config.lconfig_add_settings(&[GPIO]);
+            config.request_lines().unwrap();
+
+            // Generate events
+            trigger_falling_and_rising_edge(config.sim(), GPIO);
+
+            // Rising event
+            assert!(config
+                .request()
+                .wait_edge_events(Some(Duration::from_secs(1)))
+                .unwrap());
+
+            let mut events = config.request().read_edge_events(&mut buf).unwrap();
+            assert_eq!(events.len(), 1);
+
+            let event = events.next().unwrap().unwrap();
+            assert_eq!(event.event_type().unwrap(), EdgeKind::Rising);
+            assert_eq!(event.line_offset(), GPIO);
+
+            // No events available
+            assert!(!config
+                .request()
+                .wait_edge_events(Some(Duration::from_millis(100)))
+                .unwrap());
+        }
+
+        #[test]
+        fn falling_edge() {
+            const GPIO: Offset = 7;
+            let mut buf = request::Buffer::new(0).unwrap();
+            let mut config = TestConfig::new(NGPIO).unwrap();
+            config.lconfig_edge(None, Some(Edge::Falling));
+            config.lconfig_add_settings(&[GPIO]);
+            config.request_lines().unwrap();
+
+            // Generate events
+            trigger_falling_and_rising_edge(config.sim(), GPIO);
+
+            // Falling event
+            assert!(config
+                .request()
+                .wait_edge_events(Some(Duration::from_secs(1)))
+                .unwrap());
+
+            let mut events = config.request().read_edge_events(&mut buf).unwrap();
+            assert_eq!(events.len(), 1);
+
+            let event = events.next().unwrap().unwrap();
+            assert_eq!(event.event_type().unwrap(), EdgeKind::Falling);
+            assert_eq!(event.line_offset(), GPIO);
+
+            // No events available
+            assert!(!config
+                .request()
+                .wait_edge_events(Some(Duration::from_millis(100)))
+                .unwrap());
+        }
+
+        #[test]
+        fn edge_sequence() {
+            const GPIO: [u32; 2] = [0, 1];
+            let mut config = TestConfig::new(NGPIO).unwrap();
+            config.lconfig_edge(None, Some(Edge::Both));
+            config.lconfig_add_settings(&GPIO);
+            config.request_lines().unwrap();
+
+            // Generate events
+            trigger_rising_edge_events_on_two_offsets(config.sim(), GPIO);
+
+            // Rising event GPIO 0
+            let mut buf = request::Buffer::new(0).unwrap();
+            assert!(config
+                .request()
+                .wait_edge_events(Some(Duration::from_secs(1)))
+                .unwrap());
+
+            let mut events = config.request().read_edge_events(&mut buf).unwrap();
+            assert_eq!(events.len(), 1);
+
+            let event = events.next().unwrap().unwrap();
+            assert_eq!(event.event_type().unwrap(), EdgeKind::Rising);
+            assert_eq!(event.line_offset(), GPIO[0]);
+            assert_eq!(event.global_seqno(), 1);
+            assert_eq!(event.line_seqno(), 1);
+
+            // Rising event GPIO 1
+            assert!(config
+                .request()
+                .wait_edge_events(Some(Duration::from_secs(1)))
+                .unwrap());
+
+            let mut events = config.request().read_edge_events(&mut buf).unwrap();
+            assert_eq!(events.len(), 1);
+
+            let event = events.next().unwrap().unwrap();
+            assert_eq!(event.event_type().unwrap(), EdgeKind::Rising);
+            assert_eq!(event.line_offset(), GPIO[1]);
+            assert_eq!(event.global_seqno(), 2);
+            assert_eq!(event.line_seqno(), 1);
+
+            // No events available
+            assert!(!config
+                .request()
+                .wait_edge_events(Some(Duration::from_millis(100)))
+                .unwrap());
+        }
+
+        #[test]
+        fn multiple_events() {
+            const GPIO: Offset = 1;
+            let mut buf = request::Buffer::new(0).unwrap();
+            let mut config = TestConfig::new(NGPIO).unwrap();
+            config.lconfig_edge(None, Some(Edge::Both));
+            config.lconfig_add_settings(&[GPIO]);
+            config.request_lines().unwrap();
+
+            // Generate events
+            trigger_multiple_events(config.sim(), GPIO);
+
+            // Read multiple events
+            assert!(config
+                .request()
+                .wait_edge_events(Some(Duration::from_secs(1)))
+                .unwrap());
+
+            let events = config.request().read_edge_events(&mut buf).unwrap();
+            assert_eq!(events.len(), 3);
+
+            let mut global_seqno = 1;
+            let mut line_seqno = 1;
+
+            // Verify sequence number of events
+            for event in events {
+                let event = event.unwrap();
+                assert_eq!(event.line_offset(), GPIO);
+                assert_eq!(event.global_seqno(), global_seqno);
+                assert_eq!(event.line_seqno(), line_seqno);
+
+                global_seqno += 1;
+                line_seqno += 1;
+            }
+        }
+
+        #[test]
+        fn over_capacity() {
+            const GPIO: Offset = 2;
+            let mut buf = request::Buffer::new(2).unwrap();
+            let mut config = TestConfig::new(NGPIO).unwrap();
+            config.lconfig_edge(None, Some(Edge::Both));
+            config.lconfig_add_settings(&[GPIO]);
+            config.request_lines().unwrap();
+
+            // Generate events
+            trigger_multiple_events(config.sim(), GPIO);
+
+            // Read multiple events
+            assert!(config
+                .request()
+                .wait_edge_events(Some(Duration::from_secs(1)))
+                .unwrap());
+
+            let events = config.request().read_edge_events(&mut buf).unwrap();
+            assert_eq!(events.len(), 2);
+        }
+    }
+}
diff --git a/bindings/rust/libgpiod/tests/info_event.rs b/bindings/rust/libgpiod/tests/info_event.rs
new file mode 100644 (file)
index 0000000..c969af7
--- /dev/null
@@ -0,0 +1,166 @@
+// SPDX-License-Identifier: Apache-2.0 OR BSD-3-Clause
+// SPDX-FileCopyrightText: 2022 Linaro Ltd.
+// SPDX-FileCopyrightText: 2022 Viresh Kumar <viresh.kumar@linaro.org>
+
+mod common;
+
+mod info_event {
+    use libc::EINVAL;
+    use std::{
+        sync::{
+            mpsc::{self, Receiver, Sender},
+            Arc, Mutex,
+        },
+        thread,
+        time::Duration,
+    };
+
+    use gpiosim_sys::Sim;
+    use libgpiod::{
+        chip::Chip,
+        line::{self, Direction, InfoChangeKind, Offset},
+        request, Error as ChipError, OperationType,
+    };
+
+    fn request_reconfigure_line(chip: Arc<Mutex<Chip>>, tx: Sender<()>, rx: Receiver<()>) {
+        thread::spawn(move || {
+            let mut lconfig1 = line::Config::new().unwrap();
+            let lsettings = line::Settings::new().unwrap();
+            lconfig1.add_line_settings(&[7], lsettings).unwrap();
+            let rconfig = request::Config::new().unwrap();
+
+            let mut request = chip
+                .lock()
+                .unwrap()
+                .request_lines(Some(&rconfig), &lconfig1)
+                .unwrap();
+
+            // Signal the parent to continue
+            tx.send(()).expect("Could not send signal on channel");
+
+            // Wait for parent to signal
+            rx.recv().expect("Could not receive from channel");
+
+            let mut lconfig2 = line::Config::new().unwrap();
+            let mut lsettings = line::Settings::new().unwrap();
+            lsettings.set_direction(Direction::Output).unwrap();
+            lconfig2.add_line_settings(&[7], lsettings).unwrap();
+
+            request.reconfigure_lines(&lconfig2).unwrap();
+
+            // Signal the parent to continue
+            tx.send(()).expect("Could not send signal on channel");
+
+            // Wait for parent to signal
+            rx.recv().expect("Could not receive from channel");
+        });
+    }
+
+    mod watch {
+        use super::*;
+        const NGPIO: usize = 8;
+        const GPIO: Offset = 7;
+
+        #[test]
+        fn line_info() {
+            let sim = Sim::new(Some(NGPIO), None, true).unwrap();
+            let chip = Chip::open(&sim.dev_path()).unwrap();
+
+            assert_eq!(
+                chip.watch_line_info(NGPIO as u32).unwrap_err(),
+                ChipError::OperationFailed(OperationType::ChipWatchLineInfo, errno::Errno(EINVAL))
+            );
+
+            let info = chip.watch_line_info(GPIO).unwrap();
+            assert_eq!(info.offset(), GPIO);
+
+            // No events available
+            assert!(!chip
+                .wait_info_event(Some(Duration::from_millis(100)))
+                .unwrap());
+        }
+
+        #[test]
+        fn reconfigure() {
+            let sim = Sim::new(Some(NGPIO), None, true).unwrap();
+            let chip = Arc::new(Mutex::new(Chip::open(&sim.dev_path()).unwrap()));
+            let info = chip.lock().unwrap().watch_line_info(GPIO).unwrap();
+
+            assert_eq!(info.direction().unwrap(), Direction::Input);
+
+            // Thread synchronizing mechanism
+            let (tx_main, rx_thread) = mpsc::channel();
+            let (tx_thread, rx_main) = mpsc::channel();
+
+            // Generate events
+            request_reconfigure_line(chip.clone(), tx_thread, rx_thread);
+
+            // Wait for thread to signal
+            rx_main.recv().expect("Could not receive from channel");
+
+            // Line requested event
+            assert!(chip
+                .lock()
+                .unwrap()
+                .wait_info_event(Some(Duration::from_secs(1)))
+                .unwrap());
+            let event = chip.lock().unwrap().read_info_event().unwrap();
+            let ts_req = event.timestamp();
+
+            assert_eq!(event.event_type().unwrap(), InfoChangeKind::LineRequested);
+            assert_eq!(
+                event.line_info().unwrap().direction().unwrap(),
+                Direction::Input
+            );
+
+            // Signal the thread to continue
+            tx_main.send(()).expect("Could not send signal on channel");
+
+            // Wait for thread to signal
+            rx_main.recv().expect("Could not receive from channel");
+
+            // Line changed event
+            assert!(chip
+                .lock()
+                .unwrap()
+                .wait_info_event(Some(Duration::from_secs(1)))
+                .unwrap());
+            let event = chip.lock().unwrap().read_info_event().unwrap();
+            let ts_rec = event.timestamp();
+
+            assert_eq!(
+                event.event_type().unwrap(),
+                InfoChangeKind::LineConfigChanged
+            );
+            assert_eq!(
+                event.line_info().unwrap().direction().unwrap(),
+                Direction::Output
+            );
+
+            // Signal the thread to continue
+            tx_main.send(()).expect("Could not send signal on channel");
+
+            // Line released event
+            assert!(chip
+                .lock()
+                .unwrap()
+                .wait_info_event(Some(Duration::from_secs(1)))
+                .unwrap());
+            let event = chip.lock().unwrap().read_info_event().unwrap();
+            let ts_rel = event.timestamp();
+
+            assert_eq!(event.event_type().unwrap(), InfoChangeKind::LineReleased);
+
+            // No events available
+            assert!(!chip
+                .lock()
+                .unwrap()
+                .wait_info_event(Some(Duration::from_millis(100)))
+                .unwrap());
+
+            // Check timestamps are really monotonic.
+            assert!(ts_rel > ts_rec);
+            assert!(ts_rec > ts_req);
+        }
+    }
+}
diff --git a/bindings/rust/libgpiod/tests/line_config.rs b/bindings/rust/libgpiod/tests/line_config.rs
new file mode 100644 (file)
index 0000000..7fccf8c
--- /dev/null
@@ -0,0 +1,141 @@
+// SPDX-License-Identifier: Apache-2.0 OR BSD-3-Clause
+// SPDX-FileCopyrightText: 2022 Linaro Ltd.
+// SPDX-FileCopyrightText: 2022 Viresh Kumar <viresh.kumar@linaro.org>
+
+mod common;
+
+mod line_config {
+    use gpiosim_sys::Sim;
+    use libgpiod::chip::Chip;
+    use libgpiod::line::{
+        self, Bias, Direction, Drive, Edge, EventClock, SettingKind, SettingVal, Value,
+    };
+
+    #[test]
+    fn settings() {
+        let mut lsettings1 = line::Settings::new().unwrap();
+        lsettings1
+            .set_direction(Direction::Input)
+            .unwrap()
+            .set_edge_detection(Some(Edge::Both))
+            .unwrap()
+            .set_bias(Some(Bias::PullDown))
+            .unwrap()
+            .set_drive(Drive::PushPull)
+            .unwrap();
+
+        let mut lsettings2 = line::Settings::new().unwrap();
+        lsettings2
+            .set_direction(Direction::Output)
+            .unwrap()
+            .set_active_low(true)
+            .set_event_clock(EventClock::Realtime)
+            .unwrap()
+            .set_output_value(Value::Active)
+            .unwrap();
+
+        // Add settings for multiple lines
+        let mut lconfig = line::Config::new().unwrap();
+        lconfig.add_line_settings(&[0, 1, 2], lsettings1).unwrap();
+        lconfig.add_line_settings(&[4, 5], lsettings2).unwrap();
+
+        let settings_map = lconfig.line_settings().unwrap();
+
+        // Retrieve settings
+        let lsettings = settings_map.get(1).unwrap();
+        assert_eq!(
+            lsettings.prop(SettingKind::Direction).unwrap(),
+            SettingVal::Direction(Direction::Input)
+        );
+        assert_eq!(
+            lsettings.prop(SettingKind::EdgeDetection).unwrap(),
+            SettingVal::EdgeDetection(Some(Edge::Both))
+        );
+        assert_eq!(
+            lsettings.prop(SettingKind::Bias).unwrap(),
+            SettingVal::Bias(Some(Bias::PullDown))
+        );
+        assert_eq!(
+            lsettings.prop(SettingKind::Drive).unwrap(),
+            SettingVal::Drive(Drive::PushPull)
+        );
+
+        let lsettings = settings_map.get(5).unwrap();
+        assert_eq!(
+            lsettings.prop(SettingKind::Direction).unwrap(),
+            SettingVal::Direction(Direction::Output)
+        );
+        assert_eq!(
+            lsettings.prop(SettingKind::ActiveLow).unwrap(),
+            SettingVal::ActiveLow(true)
+        );
+        assert_eq!(
+            lsettings.prop(SettingKind::EventClock).unwrap(),
+            SettingVal::EventClock(EventClock::Realtime)
+        );
+        assert_eq!(
+            lsettings.prop(SettingKind::OutputValue).unwrap(),
+            SettingVal::OutputValue(Value::Active)
+        );
+    }
+
+    #[test]
+    fn set_global_output_values() {
+        let sim = Sim::new(Some(4), None, true).unwrap();
+        let mut settings = line::Settings::new().unwrap();
+        settings.set_direction(Direction::Output).unwrap();
+
+        let mut config = line::Config::new().unwrap();
+        config
+            .add_line_settings(&[0, 1, 2, 3], settings)
+            .unwrap()
+            .set_output_values(&[
+                Value::Active,
+                Value::InActive,
+                Value::Active,
+                Value::InActive,
+            ])
+            .unwrap();
+
+        let chip = Chip::open(&sim.dev_path()).unwrap();
+        let _request = chip.request_lines(None, &config);
+
+        assert_eq!(sim.val(0).unwrap(), gpiosim_sys::Value::Active);
+        assert_eq!(sim.val(1).unwrap(), gpiosim_sys::Value::InActive);
+        assert_eq!(sim.val(2).unwrap(), gpiosim_sys::Value::Active);
+        assert_eq!(sim.val(3).unwrap(), gpiosim_sys::Value::InActive);
+    }
+
+    #[test]
+    fn read_back_global_output_values() {
+        let mut settings = line::Settings::new().unwrap();
+        settings
+            .set_direction(Direction::Output)
+            .unwrap()
+            .set_output_value(Value::Active)
+            .unwrap();
+
+        let mut config = line::Config::new().unwrap();
+        config
+            .add_line_settings(&[0, 1, 2, 3], settings)
+            .unwrap()
+            .set_output_values(&[
+                Value::Active,
+                Value::InActive,
+                Value::Active,
+                Value::InActive,
+            ])
+            .unwrap();
+
+        assert_eq!(
+            config
+                .line_settings()
+                .unwrap()
+                .get(1)
+                .unwrap()
+                .output_value()
+                .unwrap(),
+            Value::InActive
+        );
+    }
+}
diff --git a/bindings/rust/libgpiod/tests/line_info.rs b/bindings/rust/libgpiod/tests/line_info.rs
new file mode 100644 (file)
index 0000000..d02c9ea
--- /dev/null
@@ -0,0 +1,328 @@
+// SPDX-License-Identifier: Apache-2.0 OR BSD-3-Clause
+// SPDX-FileCopyrightText: 2022 Linaro Ltd.
+// SPDX-FileCopyrightText: 2022 Viresh Kumar <viresh.kumar@linaro.org>
+
+mod common;
+
+mod line_info {
+    use libc::EINVAL;
+    use std::time::Duration;
+
+    use crate::common::*;
+    use gpiosim_sys::{Direction as SimDirection, Sim};
+    use libgpiod::{
+        chip::Chip,
+        line::{Bias, Direction, Drive, Edge, EventClock},
+        Error as ChipError, OperationType,
+    };
+
+    const NGPIO: usize = 8;
+
+    mod properties {
+        use std::thread;
+
+        use libgpiod::{line, request};
+
+        use super::*;
+
+        #[test]
+        fn default() {
+            let sim = Sim::new(Some(NGPIO), None, false).unwrap();
+            sim.set_line_name(4, "four").unwrap();
+            sim.hog_line(4, "hog4", SimDirection::OutputLow).unwrap();
+            sim.enable().unwrap();
+
+            let chip = Chip::open(&sim.dev_path()).unwrap();
+
+            let info4 = chip.line_info(4).unwrap();
+            assert_eq!(info4.offset(), 4);
+            assert_eq!(info4.name().unwrap(), "four");
+            assert!(info4.is_used());
+            assert_eq!(info4.consumer().unwrap(), "hog4");
+            assert_eq!(info4.direction().unwrap(), Direction::Output);
+            assert!(!info4.is_active_low());
+            assert_eq!(info4.bias().unwrap(), None);
+            assert_eq!(info4.drive().unwrap(), Drive::PushPull);
+            assert_eq!(info4.edge_detection().unwrap(), None);
+            assert_eq!(info4.event_clock().unwrap(), EventClock::Monotonic);
+            assert!(!info4.is_debounced());
+            assert_eq!(info4.debounce_period(), Duration::from_millis(0));
+
+            assert_eq!(
+                chip.line_info(NGPIO as u32).unwrap_err(),
+                ChipError::OperationFailed(OperationType::ChipGetLineInfo, errno::Errno(EINVAL))
+            );
+        }
+
+        #[test]
+        fn name_and_offset() {
+            let sim = Sim::new(Some(NGPIO), None, false).unwrap();
+
+            // Line 0 has no name
+            for i in 1..NGPIO {
+                sim.set_line_name(i as u32, &i.to_string()).unwrap();
+            }
+            sim.enable().unwrap();
+
+            let chip = Chip::open(&sim.dev_path()).unwrap();
+            let info = chip.line_info(0).unwrap();
+
+            assert_eq!(info.offset(), 0);
+            assert_eq!(
+                info.name().unwrap_err(),
+                ChipError::NullString("GPIO line's name")
+            );
+
+            for i in 1..NGPIO {
+                let info = chip.line_info(i as u32).unwrap();
+
+                assert_eq!(info.offset(), i as u32);
+                assert_eq!(info.name().unwrap(), &i.to_string());
+            }
+        }
+
+        #[test]
+        fn is_used() {
+            let sim = Sim::new(Some(NGPIO), None, false).unwrap();
+            sim.hog_line(0, "hog", SimDirection::OutputHigh).unwrap();
+            sim.enable().unwrap();
+
+            let chip = Chip::open(&sim.dev_path()).unwrap();
+
+            let info = chip.line_info(0).unwrap();
+            assert!(info.is_used());
+
+            let info = chip.line_info(1).unwrap();
+            assert!(!info.is_used());
+        }
+
+        #[test]
+        fn consumer() {
+            let sim = Sim::new(Some(NGPIO), None, false).unwrap();
+            sim.hog_line(0, "hog", SimDirection::OutputHigh).unwrap();
+            sim.enable().unwrap();
+
+            let chip = Chip::open(&sim.dev_path()).unwrap();
+
+            let info = chip.line_info(0).unwrap();
+            assert_eq!(info.consumer().unwrap(), "hog");
+
+            let info = chip.line_info(1).unwrap();
+            assert_eq!(
+                info.consumer().unwrap_err(),
+                ChipError::NullString("GPIO line's consumer name")
+            );
+        }
+
+        #[test]
+        fn direction() {
+            let sim = Sim::new(Some(NGPIO), None, false).unwrap();
+            sim.hog_line(0, "hog", SimDirection::Input).unwrap();
+            sim.hog_line(1, "hog", SimDirection::OutputHigh).unwrap();
+            sim.hog_line(2, "hog", SimDirection::OutputLow).unwrap();
+            sim.enable().unwrap();
+
+            let chip = Chip::open(&sim.dev_path()).unwrap();
+
+            let info = chip.line_info(0).unwrap();
+            assert_eq!(info.direction().unwrap(), Direction::Input);
+
+            let info = chip.line_info(1).unwrap();
+            assert_eq!(info.direction().unwrap(), Direction::Output);
+
+            let info = chip.line_info(2).unwrap();
+            assert_eq!(info.direction().unwrap(), Direction::Output);
+        }
+
+        #[test]
+        fn bias() {
+            let mut config = TestConfig::new(NGPIO).unwrap();
+            config.lconfig_add_settings(&[0]);
+            config.request_lines().unwrap();
+            let info = config.chip().line_info(0).unwrap();
+            assert_eq!(info.bias().unwrap(), None);
+
+            let mut config = TestConfig::new(NGPIO).unwrap();
+            config.lconfig_bias(Direction::Input, Some(Bias::PullUp));
+            config.lconfig_add_settings(&[0]);
+            config.request_lines().unwrap();
+            let info = config.chip().line_info(0).unwrap();
+            assert_eq!(info.bias().unwrap(), Some(Bias::PullUp));
+
+            let mut config = TestConfig::new(NGPIO).unwrap();
+            config.lconfig_bias(Direction::Input, Some(Bias::PullDown));
+            config.lconfig_add_settings(&[0]);
+            config.request_lines().unwrap();
+            let info = config.chip().line_info(0).unwrap();
+            assert_eq!(info.bias().unwrap(), Some(Bias::PullDown));
+
+            let mut config = TestConfig::new(NGPIO).unwrap();
+            config.lconfig_bias(Direction::Input, Some(Bias::Disabled));
+            config.lconfig_add_settings(&[0]);
+            config.request_lines().unwrap();
+            let info = config.chip().line_info(0).unwrap();
+            assert_eq!(info.bias().unwrap(), Some(Bias::Disabled));
+        }
+
+        #[test]
+        fn drive() {
+            let mut config = TestConfig::new(NGPIO).unwrap();
+            config.lconfig_add_settings(&[0]);
+            config.request_lines().unwrap();
+            let info = config.chip().line_info(0).unwrap();
+            assert_eq!(info.drive().unwrap(), Drive::PushPull);
+
+            let mut config = TestConfig::new(NGPIO).unwrap();
+            config.lconfig_drive(Direction::Input, Drive::PushPull);
+            config.lconfig_add_settings(&[0]);
+            config.request_lines().unwrap();
+            let info = config.chip().line_info(0).unwrap();
+            assert_eq!(info.drive().unwrap(), Drive::PushPull);
+
+            let mut config = TestConfig::new(NGPIO).unwrap();
+            config.lconfig_drive(Direction::Output, Drive::OpenDrain);
+            config.lconfig_add_settings(&[0]);
+            config.request_lines().unwrap();
+            let info = config.chip().line_info(0).unwrap();
+            assert_eq!(info.drive().unwrap(), Drive::OpenDrain);
+
+            let mut config = TestConfig::new(NGPIO).unwrap();
+            config.lconfig_drive(Direction::Output, Drive::OpenSource);
+            config.lconfig_add_settings(&[0]);
+            config.request_lines().unwrap();
+            let info = config.chip().line_info(0).unwrap();
+            assert_eq!(info.drive().unwrap(), Drive::OpenSource);
+        }
+
+        #[test]
+        fn edge() {
+            let mut config = TestConfig::new(NGPIO).unwrap();
+            config.lconfig_add_settings(&[0]);
+            config.request_lines().unwrap();
+            let info = config.chip().line_info(0).unwrap();
+            assert_eq!(info.edge_detection().unwrap(), None);
+
+            let mut config = TestConfig::new(NGPIO).unwrap();
+            config.lconfig_edge(Some(Direction::Input), Some(Edge::Both));
+            config.lconfig_add_settings(&[0]);
+            config.request_lines().unwrap();
+            let info = config.chip().line_info(0).unwrap();
+            assert_eq!(info.edge_detection().unwrap(), Some(Edge::Both));
+
+            let mut config = TestConfig::new(NGPIO).unwrap();
+            config.lconfig_edge(Some(Direction::Input), Some(Edge::Rising));
+            config.lconfig_add_settings(&[0]);
+            config.request_lines().unwrap();
+            let info = config.chip().line_info(0).unwrap();
+            assert_eq!(info.edge_detection().unwrap(), Some(Edge::Rising));
+
+            let mut config = TestConfig::new(NGPIO).unwrap();
+            config.lconfig_edge(Some(Direction::Input), Some(Edge::Falling));
+            config.lconfig_add_settings(&[0]);
+            config.request_lines().unwrap();
+            let info = config.chip().line_info(0).unwrap();
+            assert_eq!(info.edge_detection().unwrap(), Some(Edge::Falling));
+        }
+
+        #[test]
+        fn event_clock() {
+            let mut config = TestConfig::new(NGPIO).unwrap();
+            config.lconfig_add_settings(&[0]);
+            config.request_lines().unwrap();
+            let info = config.chip().line_info(0).unwrap();
+            assert_eq!(info.event_clock().unwrap(), EventClock::Monotonic);
+
+            let mut config = TestConfig::new(NGPIO).unwrap();
+            config.lconfig_clock(EventClock::Monotonic);
+            config.lconfig_add_settings(&[0]);
+            config.request_lines().unwrap();
+            let info = config.chip().line_info(0).unwrap();
+            assert_eq!(info.event_clock().unwrap(), EventClock::Monotonic);
+
+            let mut config = TestConfig::new(NGPIO).unwrap();
+            config.lconfig_clock(EventClock::Realtime);
+            config.lconfig_add_settings(&[0]);
+            config.request_lines().unwrap();
+            let info = config.chip().line_info(0).unwrap();
+            assert_eq!(info.event_clock().unwrap(), EventClock::Realtime);
+        }
+
+        #[test]
+        #[ignore]
+        fn event_clock_hte() {
+            let mut config = TestConfig::new(NGPIO).unwrap();
+            config.lconfig_clock(EventClock::HTE);
+            config.lconfig_add_settings(&[0]);
+            config.request_lines().unwrap();
+            let info = config.chip().line_info(0).unwrap();
+            assert_eq!(info.event_clock().unwrap(), EventClock::HTE);
+        }
+
+        #[test]
+        fn debounce() {
+            let mut config = TestConfig::new(NGPIO).unwrap();
+            config.lconfig_add_settings(&[0]);
+            config.request_lines().unwrap();
+            let info = config.chip().line_info(0).unwrap();
+            assert!(!info.is_debounced());
+            assert_eq!(info.debounce_period(), Duration::from_millis(0));
+
+            let mut config = TestConfig::new(NGPIO).unwrap();
+            config.lconfig_debounce(Duration::from_millis(100));
+            config.lconfig_add_settings(&[0]);
+            config.request_lines().unwrap();
+            let info = config.chip().line_info(0).unwrap();
+            assert!(info.is_debounced());
+            assert_eq!(info.debounce_period(), Duration::from_millis(100));
+        }
+
+        fn generate_line_event(chip: &Chip) {
+            let mut line_config = line::Config::new().unwrap();
+            line_config
+                .add_line_settings(&[0], line::Settings::new().unwrap())
+                .unwrap();
+
+            let mut request = chip
+                .request_lines(Some(&request::Config::new().unwrap()), &line_config)
+                .unwrap();
+
+            let mut new_line_config = line::Config::new().unwrap();
+            let mut settings = line::Settings::new().unwrap();
+            settings.set_direction(Direction::Output).unwrap();
+            new_line_config.add_line_settings(&[0], settings).unwrap();
+            request.reconfigure_lines(&new_line_config).unwrap();
+        }
+
+        #[test]
+        fn ownership() {
+            let sim = Sim::new(Some(1), None, false).unwrap();
+            sim.set_line_name(0, "Test line").unwrap();
+            sim.enable().unwrap();
+
+            let chip = Chip::open(&sim.dev_path()).unwrap();
+
+            // start watching line
+            chip.watch_line_info(0).unwrap();
+
+            generate_line_event(&chip);
+
+            // read generated event
+            let event = chip.read_info_event().unwrap();
+            let info = event.line_info().unwrap();
+            assert_eq!(info.name().unwrap(), "Test line");
+
+            // clone info and move to separate thread
+            let info = info.try_clone().unwrap();
+
+            // drop the original event with the associated line_info
+            drop(event);
+
+            // assert that we can still read the name
+            thread::scope(|s| {
+                s.spawn(move || {
+                    assert_eq!(info.name().unwrap(), "Test line");
+                });
+            });
+        }
+    }
+}
diff --git a/bindings/rust/libgpiod/tests/line_request.rs b/bindings/rust/libgpiod/tests/line_request.rs
new file mode 100644 (file)
index 0000000..a936a1b
--- /dev/null
@@ -0,0 +1,517 @@
+// SPDX-License-Identifier: Apache-2.0 OR BSD-3-Clause
+// SPDX-FileCopyrightText: 2022 Linaro Ltd.
+// SPDX-FileCopyrightText: 2022 Viresh Kumar <viresh.kumar@linaro.org>
+
+mod common;
+
+mod line_request {
+    use libc::{E2BIG, EINVAL};
+    use std::time::Duration;
+
+    use crate::common::*;
+    use gpiosim_sys::{Pull, Value as SimValue};
+    use libgpiod::{
+        line::{
+            self, Bias, Direction, Drive, Edge, EventClock, Offset, SettingVal, Value, ValueMap,
+        },
+        Error as ChipError, OperationType,
+    };
+
+    const NGPIO: usize = 8;
+
+    mod invalid_arguments {
+        use super::*;
+
+        #[test]
+        fn no_offsets() {
+            let mut config = TestConfig::new(NGPIO).unwrap();
+
+            assert_eq!(
+                config.request_lines().unwrap_err(),
+                ChipError::OperationFailed(OperationType::ChipRequestLines, errno::Errno(EINVAL))
+            );
+        }
+
+        #[test]
+        fn out_of_bound_offsets() {
+            let mut config = TestConfig::new(NGPIO).unwrap();
+            config.lconfig_add_settings(&[2, 0, 8, 4]);
+
+            assert_eq!(
+                config.request_lines().unwrap_err(),
+                ChipError::OperationFailed(OperationType::ChipRequestLines, errno::Errno(EINVAL))
+            );
+        }
+
+        #[test]
+        fn dir_out_edge_failure() {
+            let mut config = TestConfig::new(NGPIO).unwrap();
+            config.lconfig_edge(Some(Direction::Output), Some(Edge::Both));
+            config.lconfig_add_settings(&[0]);
+
+            assert_eq!(
+                config.request_lines().unwrap_err(),
+                ChipError::OperationFailed(OperationType::ChipRequestLines, errno::Errno(EINVAL))
+            );
+        }
+    }
+
+    mod verify {
+        use super::*;
+
+        #[test]
+        #[cfg(feature = "vnext")]
+        fn chip_name() {
+            const GPIO: Offset = 2;
+            let mut config = TestConfig::new(NGPIO).unwrap();
+            config.lconfig_add_settings(&[GPIO]);
+            config.request_lines().unwrap();
+
+            let arc = config.sim();
+            let sim = arc.lock().unwrap();
+            let chip_name = sim.chip_name();
+
+            assert_eq!(config.request().chip_name().unwrap(), chip_name);
+        }
+
+        #[test]
+        fn custom_consumer() {
+            const GPIO: Offset = 2;
+            const CONSUMER: &str = "foobar";
+            let mut config = TestConfig::new(NGPIO).unwrap();
+            config.rconfig_set_consumer(CONSUMER);
+            config.lconfig_add_settings(&[GPIO]);
+            config.request_lines().unwrap();
+
+            let info = config.chip().line_info(GPIO).unwrap();
+
+            assert!(info.is_used());
+            assert_eq!(info.consumer().unwrap(), CONSUMER);
+        }
+
+        #[test]
+        fn empty_consumer() {
+            const GPIO: Offset = 2;
+            let mut config = TestConfig::new(NGPIO).unwrap();
+            config.lconfig_add_settings(&[GPIO]);
+            config.request_lines().unwrap();
+
+            let info = config.chip().line_info(GPIO).unwrap();
+
+            assert!(info.is_used());
+            assert_eq!(info.consumer().unwrap(), "?");
+        }
+
+        #[test]
+        fn read_values() {
+            let offsets = [7, 1, 0, 6, 2];
+            let pulls = [Pull::Up, Pull::Up, Pull::Down, Pull::Up, Pull::Down];
+            let mut config = TestConfig::new(NGPIO).unwrap();
+            config.set_pull(&offsets, &pulls);
+            config.lconfig_val(Some(Direction::Input), None);
+            config.lconfig_add_settings(&offsets);
+            config.request_lines().unwrap();
+
+            let request = config.request();
+
+            // Single values read properly
+            assert_eq!(request.value(7).unwrap(), Value::Active);
+
+            // Values read properly
+            let map = request.values().unwrap();
+            for i in 0..offsets.len() {
+                assert_eq!(
+                    *map.get(offsets[i].into()).unwrap(),
+                    match pulls[i] {
+                        Pull::Down => Value::InActive,
+                        _ => Value::Active,
+                    }
+                );
+            }
+
+            // Subset of values read properly
+            let map = request.values_subset(&[2, 0, 6]).unwrap();
+            assert_eq!(*map.get(2).unwrap(), Value::InActive);
+            assert_eq!(*map.get(0).unwrap(), Value::InActive);
+            assert_eq!(*map.get(6).unwrap(), Value::Active);
+
+            // Value read properly after reconfigure
+            let mut lsettings = line::Settings::new().unwrap();
+            lsettings.set_active_low(true);
+            let mut lconfig = line::Config::new().unwrap();
+            lconfig.add_line_settings(&offsets, lsettings).unwrap();
+            request.reconfigure_lines(&lconfig).unwrap();
+            assert_eq!(request.value(7).unwrap(), Value::InActive);
+        }
+
+        #[test]
+        fn set_output_values() {
+            let offsets = [0, 1, 3, 4];
+            let mut config = TestConfig::new(NGPIO).unwrap();
+            config.lconfig_val(Some(Direction::Output), Some(Value::Active));
+            config.lconfig_add_settings(&offsets);
+            config.request_lines().unwrap();
+
+            assert_eq!(config.sim_val(0).unwrap(), SimValue::Active);
+            assert_eq!(config.sim_val(1).unwrap(), SimValue::Active);
+            assert_eq!(config.sim_val(3).unwrap(), SimValue::Active);
+            assert_eq!(config.sim_val(4).unwrap(), SimValue::Active);
+
+            // Default
+            assert_eq!(config.sim_val(2).unwrap(), SimValue::InActive);
+        }
+
+        #[test]
+        fn update_output_values() {
+            let offsets = [0, 1, 3, 4];
+            let mut config = TestConfig::new(NGPIO).unwrap();
+            config.lconfig_val(Some(Direction::Output), Some(Value::InActive));
+            config.lconfig_add_settings(&offsets);
+            config.request_lines().unwrap();
+
+            // Set single value
+            config.request().set_value(1, Value::Active).unwrap();
+            assert_eq!(config.sim_val(0).unwrap(), SimValue::InActive);
+            assert_eq!(config.sim_val(1).unwrap(), SimValue::Active);
+            assert_eq!(config.sim_val(3).unwrap(), SimValue::InActive);
+            assert_eq!(config.sim_val(4).unwrap(), SimValue::InActive);
+            config.request().set_value(1, Value::InActive).unwrap();
+            assert_eq!(config.sim_val(1).unwrap(), SimValue::InActive);
+
+            // Set values of subset
+            let mut map = ValueMap::new();
+            map.insert(4, Value::Active);
+            map.insert(3, Value::Active);
+            config.request().set_values_subset(map).unwrap();
+            assert_eq!(config.sim_val(0).unwrap(), SimValue::InActive);
+            assert_eq!(config.sim_val(1).unwrap(), SimValue::InActive);
+            assert_eq!(config.sim_val(3).unwrap(), SimValue::Active);
+            assert_eq!(config.sim_val(4).unwrap(), SimValue::Active);
+
+            let mut map = ValueMap::new();
+            map.insert(4, Value::InActive);
+            map.insert(3, Value::InActive);
+            config.request().set_values_subset(map).unwrap();
+            assert_eq!(config.sim_val(3).unwrap(), SimValue::InActive);
+            assert_eq!(config.sim_val(4).unwrap(), SimValue::InActive);
+
+            // Set all values
+            config
+                .request()
+                .set_values(&[
+                    Value::Active,
+                    Value::InActive,
+                    Value::Active,
+                    Value::InActive,
+                ])
+                .unwrap();
+            assert_eq!(config.sim_val(0).unwrap(), SimValue::Active);
+            assert_eq!(config.sim_val(1).unwrap(), SimValue::InActive);
+            assert_eq!(config.sim_val(3).unwrap(), SimValue::Active);
+            assert_eq!(config.sim_val(4).unwrap(), SimValue::InActive);
+            config
+                .request()
+                .set_values(&[
+                    Value::InActive,
+                    Value::InActive,
+                    Value::InActive,
+                    Value::InActive,
+                ])
+                .unwrap();
+            assert_eq!(config.sim_val(0).unwrap(), SimValue::InActive);
+            assert_eq!(config.sim_val(1).unwrap(), SimValue::InActive);
+            assert_eq!(config.sim_val(3).unwrap(), SimValue::InActive);
+            assert_eq!(config.sim_val(4).unwrap(), SimValue::InActive);
+        }
+
+        #[test]
+        fn set_bias() {
+            let offsets = [3];
+            let mut config = TestConfig::new(NGPIO).unwrap();
+            config.lconfig_bias(Direction::Input, Some(Bias::PullUp));
+            config.lconfig_add_settings(&offsets);
+            config.request_lines().unwrap();
+            config.request();
+
+            // Set single value
+            assert_eq!(config.sim_val(3).unwrap(), SimValue::Active);
+        }
+
+        #[test]
+        fn no_events() {
+            let mut config = TestConfig::new(NGPIO).unwrap();
+            config.lconfig_edge(None, Some(Edge::Both));
+            config.lconfig_add_settings(&[0]);
+            config.request_lines().unwrap();
+
+            // No events available
+            assert!(!config
+                .request()
+                .wait_edge_events(Some(Duration::from_millis(100)))
+                .unwrap());
+        }
+    }
+
+    mod reconfigure {
+        use super::*;
+
+        #[test]
+        fn e2big() {
+            let offsets = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
+            let mut config = TestConfig::new(16).unwrap();
+            config.lconfig_add_settings(&offsets);
+            config.request_lines().unwrap();
+
+            let request = config.request();
+
+            // Reconfigure
+            let mut lsettings = line::Settings::new().unwrap();
+            lsettings.set_direction(Direction::Input).unwrap();
+            let mut lconfig = line::Config::new().unwrap();
+
+            // The uAPI config has only 10 attribute slots, this should pass.
+            for offset in offsets {
+                lsettings.set_debounce_period(Duration::from_millis((100 + offset).into()));
+                lconfig
+                    .add_line_settings(&[offset as Offset], lsettings.try_clone().unwrap())
+                    .unwrap();
+            }
+
+            assert!(request.reconfigure_lines(&lconfig).is_ok());
+
+            // The uAPI config has only 10 attribute slots, and this is the 11th entry.
+            // This should fail with E2BIG.
+            lsettings.set_debounce_period(Duration::from_millis(100 + 11));
+            lconfig.add_line_settings(&[11], lsettings).unwrap();
+
+            assert_eq!(
+                request.reconfigure_lines(&lconfig).unwrap_err(),
+                ChipError::OperationFailed(
+                    OperationType::LineRequestReconfigLines,
+                    errno::Errno(E2BIG),
+                )
+            );
+        }
+
+        #[test]
+        fn bias() {
+            let mut config = TestConfig::new(NGPIO).unwrap();
+            config.lconfig_add_settings(&[0]);
+            config.request_lines().unwrap();
+            let info = config.chip().line_info(0).unwrap();
+            assert_eq!(info.bias().unwrap(), None);
+
+            // Reconfigure
+            let mut lconfig = line::Config::new().unwrap();
+            let mut lsettings = line::Settings::new().unwrap();
+            lsettings
+                .set_prop(&[
+                    SettingVal::Direction(Direction::Input),
+                    SettingVal::Bias(Some(Bias::PullUp)),
+                ])
+                .unwrap();
+            lconfig.add_line_settings(&[0], lsettings).unwrap();
+            config.request().reconfigure_lines(&lconfig).unwrap();
+            let info = config.chip().line_info(0).unwrap();
+            assert_eq!(info.bias().unwrap(), Some(Bias::PullUp));
+
+            let mut lconfig = line::Config::new().unwrap();
+            let mut lsettings = line::Settings::new().unwrap();
+            lsettings
+                .set_prop(&[
+                    SettingVal::Direction(Direction::Input),
+                    SettingVal::Bias(Some(Bias::PullDown)),
+                ])
+                .unwrap();
+            lconfig.add_line_settings(&[0], lsettings).unwrap();
+            config.request().reconfigure_lines(&lconfig).unwrap();
+            let info = config.chip().line_info(0).unwrap();
+            assert_eq!(info.bias().unwrap(), Some(Bias::PullDown));
+
+            let mut lconfig = line::Config::new().unwrap();
+            let mut lsettings = line::Settings::new().unwrap();
+            lsettings
+                .set_prop(&[
+                    SettingVal::Direction(Direction::Input),
+                    SettingVal::Bias(Some(Bias::Disabled)),
+                ])
+                .unwrap();
+            lconfig.add_line_settings(&[0], lsettings).unwrap();
+            config.request().reconfigure_lines(&lconfig).unwrap();
+            let info = config.chip().line_info(0).unwrap();
+            assert_eq!(info.bias().unwrap(), Some(Bias::Disabled));
+        }
+
+        #[test]
+        fn drive() {
+            let mut config = TestConfig::new(NGPIO).unwrap();
+            config.lconfig_add_settings(&[0]);
+            config.request_lines().unwrap();
+            let info = config.chip().line_info(0).unwrap();
+            assert_eq!(info.drive().unwrap(), Drive::PushPull);
+
+            // Reconfigure
+            let mut lconfig = line::Config::new().unwrap();
+            let mut lsettings = line::Settings::new().unwrap();
+            lsettings
+                .set_prop(&[
+                    SettingVal::Direction(Direction::Input),
+                    SettingVal::Drive(Drive::PushPull),
+                ])
+                .unwrap();
+            lconfig.add_line_settings(&[0], lsettings).unwrap();
+            config.request().reconfigure_lines(&lconfig).unwrap();
+            let info = config.chip().line_info(0).unwrap();
+            assert_eq!(info.drive().unwrap(), Drive::PushPull);
+
+            let mut lconfig = line::Config::new().unwrap();
+            let mut lsettings = line::Settings::new().unwrap();
+            lsettings
+                .set_prop(&[
+                    SettingVal::Direction(Direction::Output),
+                    SettingVal::Drive(Drive::OpenDrain),
+                ])
+                .unwrap();
+            lconfig.add_line_settings(&[0], lsettings).unwrap();
+            config.request().reconfigure_lines(&lconfig).unwrap();
+            let info = config.chip().line_info(0).unwrap();
+            assert_eq!(info.drive().unwrap(), Drive::OpenDrain);
+
+            let mut lconfig = line::Config::new().unwrap();
+            let mut lsettings = line::Settings::new().unwrap();
+            lsettings
+                .set_prop(&[
+                    SettingVal::Direction(Direction::Output),
+                    SettingVal::Drive(Drive::OpenSource),
+                ])
+                .unwrap();
+            lconfig.add_line_settings(&[0], lsettings).unwrap();
+            config.request().reconfigure_lines(&lconfig).unwrap();
+            let info = config.chip().line_info(0).unwrap();
+            assert_eq!(info.drive().unwrap(), Drive::OpenSource);
+        }
+
+        #[test]
+        fn edge() {
+            let mut config = TestConfig::new(NGPIO).unwrap();
+            config.lconfig_add_settings(&[0]);
+            config.request_lines().unwrap();
+            let info = config.chip().line_info(0).unwrap();
+            assert_eq!(info.edge_detection().unwrap(), None);
+
+            // Reconfigure
+            let mut lconfig = line::Config::new().unwrap();
+            let mut lsettings = line::Settings::new().unwrap();
+            lsettings
+                .set_prop(&[
+                    SettingVal::Direction(Direction::Input),
+                    SettingVal::EdgeDetection(Some(Edge::Both)),
+                ])
+                .unwrap();
+            lconfig.add_line_settings(&[0], lsettings).unwrap();
+            config.request().reconfigure_lines(&lconfig).unwrap();
+            let info = config.chip().line_info(0).unwrap();
+            assert_eq!(info.edge_detection().unwrap(), Some(Edge::Both));
+
+            let mut lconfig = line::Config::new().unwrap();
+            let mut lsettings = line::Settings::new().unwrap();
+            lsettings
+                .set_prop(&[
+                    SettingVal::Direction(Direction::Input),
+                    SettingVal::EdgeDetection(Some(Edge::Rising)),
+                ])
+                .unwrap();
+            lconfig.add_line_settings(&[0], lsettings).unwrap();
+            config.request().reconfigure_lines(&lconfig).unwrap();
+            let info = config.chip().line_info(0).unwrap();
+            assert_eq!(info.edge_detection().unwrap(), Some(Edge::Rising));
+
+            let mut lconfig = line::Config::new().unwrap();
+            let mut lsettings = line::Settings::new().unwrap();
+            lsettings
+                .set_prop(&[
+                    SettingVal::Direction(Direction::Input),
+                    SettingVal::EdgeDetection(Some(Edge::Falling)),
+                ])
+                .unwrap();
+            lconfig.add_line_settings(&[0], lsettings).unwrap();
+            config.request().reconfigure_lines(&lconfig).unwrap();
+            let info = config.chip().line_info(0).unwrap();
+            assert_eq!(info.edge_detection().unwrap(), Some(Edge::Falling));
+        }
+
+        #[test]
+        fn event_clock() {
+            let mut config = TestConfig::new(NGPIO).unwrap();
+            config.lconfig_add_settings(&[0]);
+            config.request_lines().unwrap();
+            let info = config.chip().line_info(0).unwrap();
+            assert_eq!(info.event_clock().unwrap(), EventClock::Monotonic);
+
+            // Reconfigure
+            let mut lconfig = line::Config::new().unwrap();
+            let mut lsettings = line::Settings::new().unwrap();
+            lsettings.set_event_clock(EventClock::Monotonic).unwrap();
+            lconfig.add_line_settings(&[0], lsettings).unwrap();
+            config.request().reconfigure_lines(&lconfig).unwrap();
+            let info = config.chip().line_info(0).unwrap();
+            assert_eq!(info.event_clock().unwrap(), EventClock::Monotonic);
+
+            let mut lconfig = line::Config::new().unwrap();
+            let mut lsettings = line::Settings::new().unwrap();
+            lsettings.set_event_clock(EventClock::Realtime).unwrap();
+            lconfig.add_line_settings(&[0], lsettings).unwrap();
+            config.request().reconfigure_lines(&lconfig).unwrap();
+            let info = config.chip().line_info(0).unwrap();
+            assert_eq!(info.event_clock().unwrap(), EventClock::Realtime);
+        }
+
+        #[test]
+        #[ignore]
+        fn event_clock_hte() {
+            let mut config = TestConfig::new(NGPIO).unwrap();
+            config.lconfig_add_settings(&[0]);
+            config.request_lines().unwrap();
+            let info = config.chip().line_info(0).unwrap();
+            assert_eq!(info.event_clock().unwrap(), EventClock::Monotonic);
+
+            let request = config.request();
+
+            // Reconfigure
+            let mut lconfig = line::Config::new().unwrap();
+            let mut lsettings = line::Settings::new().unwrap();
+            lsettings.set_event_clock(EventClock::HTE).unwrap();
+            lconfig.add_line_settings(&[0], lsettings).unwrap();
+            request.reconfigure_lines(&lconfig).unwrap();
+            let info = config.chip().line_info(0).unwrap();
+            assert_eq!(info.event_clock().unwrap(), EventClock::HTE);
+        }
+
+        #[test]
+        fn debounce() {
+            let mut config = TestConfig::new(NGPIO).unwrap();
+            config.lconfig_add_settings(&[0]);
+            config.request_lines().unwrap();
+            let info = config.chip().line_info(0).unwrap();
+            assert!(!info.is_debounced());
+            assert_eq!(info.debounce_period(), Duration::from_millis(0));
+
+            let request = config.request();
+
+            // Reconfigure
+            let mut lconfig = line::Config::new().unwrap();
+            let mut lsettings = line::Settings::new().unwrap();
+            lsettings
+                .set_prop(&[
+                    SettingVal::Direction(Direction::Input),
+                    SettingVal::DebouncePeriod(Duration::from_millis(100)),
+                ])
+                .unwrap();
+            lconfig.add_line_settings(&[0], lsettings).unwrap();
+            request.reconfigure_lines(&lconfig).unwrap();
+            let info = config.chip().line_info(0).unwrap();
+            assert!(info.is_debounced());
+            assert_eq!(info.debounce_period(), Duration::from_millis(100));
+        }
+    }
+}
diff --git a/bindings/rust/libgpiod/tests/line_settings.rs b/bindings/rust/libgpiod/tests/line_settings.rs
new file mode 100644 (file)
index 0000000..1aaa6b4
--- /dev/null
@@ -0,0 +1,203 @@
+// SPDX-License-Identifier: Apache-2.0 OR BSD-3-Clause
+// SPDX-FileCopyrightText: 2022 Linaro Ltd.
+// SPDX-FileCopyrightText: 2022 Viresh Kumar <viresh.kumar@linaro.org>
+
+mod common;
+
+mod line_settings {
+    use std::time::Duration;
+
+    use libgpiod::line::{
+        self, Bias, Direction, Drive, Edge, EventClock, SettingKind, SettingVal, Value,
+    };
+
+    #[test]
+    fn direction() {
+        let mut lsettings = line::Settings::new().unwrap();
+        assert_eq!(
+            lsettings.prop(SettingKind::Direction).unwrap(),
+            SettingVal::Direction(Direction::AsIs)
+        );
+
+        lsettings.set_direction(Direction::Input).unwrap();
+        assert_eq!(
+            lsettings.prop(SettingKind::Direction).unwrap(),
+            SettingVal::Direction(Direction::Input)
+        );
+
+        lsettings.set_direction(Direction::Output).unwrap();
+        assert_eq!(
+            lsettings.prop(SettingKind::Direction).unwrap(),
+            SettingVal::Direction(Direction::Output)
+        );
+    }
+
+    #[test]
+    fn edge_detection() {
+        let mut lsettings = line::Settings::new().unwrap();
+        assert_eq!(
+            lsettings.prop(SettingKind::EdgeDetection).unwrap(),
+            SettingVal::EdgeDetection(None)
+        );
+
+        lsettings.set_edge_detection(Some(Edge::Both)).unwrap();
+        assert_eq!(
+            lsettings.prop(SettingKind::EdgeDetection).unwrap(),
+            SettingVal::EdgeDetection(Some(Edge::Both))
+        );
+
+        lsettings.set_edge_detection(Some(Edge::Rising)).unwrap();
+        assert_eq!(
+            lsettings.prop(SettingKind::EdgeDetection).unwrap(),
+            SettingVal::EdgeDetection(Some(Edge::Rising))
+        );
+
+        lsettings.set_edge_detection(Some(Edge::Falling)).unwrap();
+        assert_eq!(
+            lsettings.prop(SettingKind::EdgeDetection).unwrap(),
+            SettingVal::EdgeDetection(Some(Edge::Falling))
+        );
+    }
+
+    #[test]
+    fn bias() {
+        let mut lsettings = line::Settings::new().unwrap();
+        assert_eq!(
+            lsettings.prop(SettingKind::Bias).unwrap(),
+            SettingVal::Bias(None)
+        );
+
+        lsettings.set_bias(Some(Bias::PullDown)).unwrap();
+        assert_eq!(
+            lsettings.prop(SettingKind::Bias).unwrap(),
+            SettingVal::Bias(Some(Bias::PullDown))
+        );
+
+        lsettings.set_bias(Some(Bias::PullUp)).unwrap();
+        assert_eq!(
+            lsettings.prop(SettingKind::Bias).unwrap(),
+            SettingVal::Bias(Some(Bias::PullUp))
+        );
+    }
+
+    #[test]
+    fn drive() {
+        let mut lsettings = line::Settings::new().unwrap();
+        assert_eq!(
+            lsettings.prop(SettingKind::Drive).unwrap(),
+            SettingVal::Drive(Drive::PushPull)
+        );
+
+        lsettings.set_drive(Drive::PushPull).unwrap();
+        assert_eq!(
+            lsettings.prop(SettingKind::Drive).unwrap(),
+            SettingVal::Drive(Drive::PushPull)
+        );
+
+        lsettings.set_drive(Drive::OpenDrain).unwrap();
+        assert_eq!(
+            lsettings.prop(SettingKind::Drive).unwrap(),
+            SettingVal::Drive(Drive::OpenDrain)
+        );
+
+        lsettings.set_drive(Drive::OpenSource).unwrap();
+        assert_eq!(
+            lsettings.prop(SettingKind::Drive).unwrap(),
+            SettingVal::Drive(Drive::OpenSource)
+        );
+    }
+
+    #[test]
+    fn active_low() {
+        let mut lsettings = line::Settings::new().unwrap();
+        assert_eq!(
+            lsettings.prop(SettingKind::ActiveLow).unwrap(),
+            SettingVal::ActiveLow(false)
+        );
+
+        lsettings.set_active_low(true);
+        assert_eq!(
+            lsettings.prop(SettingKind::ActiveLow).unwrap(),
+            SettingVal::ActiveLow(true)
+        );
+
+        lsettings.set_active_low(false);
+        assert_eq!(
+            lsettings.prop(SettingKind::ActiveLow).unwrap(),
+            SettingVal::ActiveLow(false)
+        );
+    }
+
+    #[test]
+    fn debounce_period() {
+        let mut lsettings = line::Settings::new().unwrap();
+        assert_eq!(
+            lsettings.prop(SettingKind::DebouncePeriod).unwrap(),
+            SettingVal::DebouncePeriod(Duration::from_millis(0))
+        );
+
+        lsettings.set_debounce_period(Duration::from_millis(5));
+        assert_eq!(
+            lsettings.prop(SettingKind::DebouncePeriod).unwrap(),
+            SettingVal::DebouncePeriod(Duration::from_millis(5))
+        );
+    }
+
+    #[test]
+    fn event_clock() {
+        let mut lsettings = line::Settings::new().unwrap();
+        assert_eq!(
+            lsettings.prop(SettingKind::EventClock).unwrap(),
+            SettingVal::EventClock(EventClock::Monotonic)
+        );
+
+        lsettings.set_event_clock(EventClock::Realtime).unwrap();
+        assert_eq!(
+            lsettings.prop(SettingKind::EventClock).unwrap(),
+            SettingVal::EventClock(EventClock::Realtime)
+        );
+
+        lsettings.set_event_clock(EventClock::Monotonic).unwrap();
+        assert_eq!(
+            lsettings.prop(SettingKind::EventClock).unwrap(),
+            SettingVal::EventClock(EventClock::Monotonic)
+        );
+    }
+
+    #[test]
+    #[ignore]
+    fn event_clock_hte() {
+        let mut lsettings = line::Settings::new().unwrap();
+        assert_eq!(
+            lsettings.prop(SettingKind::EventClock).unwrap(),
+            SettingVal::EventClock(EventClock::Monotonic)
+        );
+
+        lsettings.set_event_clock(EventClock::HTE).unwrap();
+        assert_eq!(
+            lsettings.prop(SettingKind::EventClock).unwrap(),
+            SettingVal::EventClock(EventClock::HTE)
+        );
+    }
+
+    #[test]
+    fn output_value() {
+        let mut lsettings = line::Settings::new().unwrap();
+        assert_eq!(
+            lsettings.prop(SettingKind::OutputValue).unwrap(),
+            SettingVal::OutputValue(Value::InActive)
+        );
+
+        lsettings.set_output_value(Value::Active).unwrap();
+        assert_eq!(
+            lsettings.prop(SettingKind::OutputValue).unwrap(),
+            SettingVal::OutputValue(Value::Active)
+        );
+
+        lsettings.set_output_value(Value::InActive).unwrap();
+        assert_eq!(
+            lsettings.prop(SettingKind::OutputValue).unwrap(),
+            SettingVal::OutputValue(Value::InActive)
+        );
+    }
+}
diff --git a/bindings/rust/libgpiod/tests/request_config.rs b/bindings/rust/libgpiod/tests/request_config.rs
new file mode 100644 (file)
index 0000000..a925a68
--- /dev/null
@@ -0,0 +1,38 @@
+// SPDX-License-Identifier: Apache-2.0 OR BSD-3-Clause
+// SPDX-FileCopyrightText: 2022 Linaro Ltd.
+// SPDX-FileCopyrightText: 2022 Viresh Kumar <viresh.kumar@linaro.org>
+
+mod common;
+
+mod request_config {
+    use libgpiod::{request, Error as ChipError, OperationType};
+
+    mod verify {
+        use super::*;
+
+        #[test]
+        fn default() {
+            let rconfig = request::Config::new().unwrap();
+
+            assert_eq!(rconfig.event_buffer_size(), 0);
+            assert_eq!(
+                rconfig.consumer().unwrap_err(),
+                ChipError::OperationFailed(
+                    OperationType::RequestConfigGetConsumer,
+                    errno::Errno(0),
+                )
+            );
+        }
+
+        #[test]
+        fn initialized() {
+            const CONSUMER: &str = "foobar";
+            let mut rconfig = request::Config::new().unwrap();
+            rconfig.set_consumer(CONSUMER).unwrap();
+            rconfig.set_event_buffer_size(64);
+
+            assert_eq!(rconfig.event_buffer_size(), 64);
+            assert_eq!(rconfig.consumer().unwrap(), CONSUMER);
+        }
+    }
+}
diff --git a/configure.ac b/configure.ac
new file mode 100644 (file)
index 0000000..d0b1479
--- /dev/null
@@ -0,0 +1,300 @@
+# SPDX-License-Identifier: GPL-2.0-or-later
+# SPDX-FileCopyrightText: 2017-2022 Bartosz Golaszewski <brgl@bgdev.pl>
+
+AC_PREREQ([2.69])
+
+AC_INIT([libgpiod], [2.1.3])
+AC_SUBST(EXTRA_VERSION, [])
+
+AC_DEFINE_UNQUOTED([GPIOD_VERSION_STR],
+                       ["$PACKAGE_VERSION$EXTRA_VERSION"],
+                       [Full library version string.])
+AC_SUBST(VERSION_STR, [$PACKAGE_VERSION$EXTRA_VERSION])
+
+# From the libtool manual:
+#
+# (...)
+# 3. If the library source code has changed at all since the last update, then
+#    increment revision ('c:r:a' becomes 'c:r+1:a').
+# 4. If any interfaces have been added, removed, or changed since the last
+#    update, increment current, and set revision to 0.
+# 5. If any interfaces have been added since the last public release, then
+#    increment age.
+# 6. If any interfaces have been removed or changed since the last public
+#    release, then set age to 0.
+#
+# Define the libtool version as (C.R.A):
+# NOTE: this version only applies to the core C library.
+AC_SUBST(ABI_VERSION, [4.2.1])
+# Have a separate ABI version for C++ bindings:
+AC_SUBST(ABI_CXX_VERSION, [3.0.1])
+# ABI version for libgpiosim (we need this since it can be installed if we
+# enable tests).
+AC_SUBST(ABI_GPIOSIM_VERSION, [1.1.0])
+
+AC_CONFIG_AUX_DIR([autostuff])
+AC_CONFIG_MACRO_DIRS([m4])
+AM_INIT_AUTOMAKE([foreign subdir-objects])
+
+m4_ifdef([AM_SILENT_RULES], [AM_SILENT_RULES([yes])])
+m4_pattern_forbid([^AX_],
+       [Unexpanded AX_ macro found. Please install GNU autoconf-archive.])
+
+AC_CONFIG_SRCDIR([lib])
+AC_CONFIG_HEADERS([config.h])
+
+AC_DEFINE([_GNU_SOURCE], [], [We want GNU extensions])
+
+# Silence warning: ar: 'u' modifier ignored since 'D' is the default
+AC_SUBST(AR_FLAGS, [cr])
+
+AM_PROG_AR
+AC_PROG_CC
+AC_PROG_CXX
+AC_PROG_INSTALL
+AC_PROG_EGREP
+
+LT_INIT
+
+AC_DEFUN([ERR_NOT_FOUND],
+       [AC_MSG_ERROR([$1 not found (needed to build $2)], [1])])
+
+AC_DEFUN([FUNC_NOT_FOUND_LIB],
+       [ERR_NOT_FOUND([$1()], [the library])])
+
+AC_DEFUN([HEADER_NOT_FOUND_LIB],
+       [ERR_NOT_FOUND([$1 header], [the library])])
+
+AC_DEFUN([HEADER_NOT_FOUND_TESTS],
+       [ERR_NOT_FOUND([$1 header], [the test suite])])
+
+AC_DEFUN([HEADER_NOT_FOUND_CXX],
+       [ERR_NOT_FOUND([$1 header], [C++ bindings])])
+
+# This is always checked (library needs this)
+AC_HEADER_STDC
+AC_FUNC_MALLOC
+AC_HEADER_STDBOOL
+AC_CHECK_FUNC([ioctl], [], [FUNC_NOT_FOUND_LIB([ioctl])])
+AC_CHECK_FUNC([open], [], [FUNC_NOT_FOUND_LIB([open])])
+AC_CHECK_FUNC([close], [], [FUNC_NOT_FOUND_LIB([close])])
+AC_CHECK_FUNC([read], [], [FUNC_NOT_FOUND_LIB([read])])
+AC_CHECK_FUNC([ppoll], [], [FUNC_NOT_FOUND_LIB([ppoll])])
+AC_CHECK_FUNC([realpath], [], [FUNC_NOT_FOUND_LIB([realpath])])
+AC_CHECK_FUNC([readlink], [], [FUNC_NOT_FOUND_LIB([readlink])])
+AC_CHECK_HEADERS([fcntl.h], [], [HEADER_NOT_FOUND_LIB([fcntl.h])])
+AC_CHECK_HEADERS([getopt.h], [], [HEADER_NOT_FOUND_LIB([getopt.h])])
+AC_CHECK_HEADERS([dirent.h], [], [HEADER_NOT_FOUND_LIB([dirent.h])])
+AC_CHECK_HEADERS([poll.h], [], [HEADER_NOT_FOUND_LIB([poll.h])])
+AC_CHECK_HEADERS([sys/sysmacros.h], [], [HEADER_NOT_FOUND_LIB([sys/sysmacros.h])])
+AC_CHECK_HEADERS([sys/ioctl.h], [], [HEADER_NOT_FOUND_LIB([sys/ioctl.h])])
+AC_CHECK_HEADERS([sys/param.h], [], [HEADER_NOT_FOUND_LIB([sys/param.h])])
+AC_CHECK_HEADERS([sys/stat.h], [], [HEADER_NOT_FOUND_LIB([sys/stat.h])])
+AC_CHECK_HEADERS([sys/types.h], [], [HEADER_NOT_FOUND_LIB([sys/types.h])])
+AC_CHECK_HEADERS([linux/const.h], [], [HEADER_NOT_FOUND_LIB([linux/const.h])])
+AC_CHECK_HEADERS([linux/ioctl.h], [], [HEADER_NOT_FOUND_LIB([linux/ioctl.h])])
+AC_CHECK_HEADERS([linux/types.h], [], [HEADER_NOT_FOUND_LIB([linux/types.h])])
+
+AC_ARG_ENABLE([tools],
+       [AS_HELP_STRING([--enable-tools],[enable libgpiod command-line tools [default=no]])],
+       [if test "x$enableval" = xyes; then with_tools=true; fi],
+       [with_tools=false])
+AM_CONDITIONAL([WITH_TOOLS], [test "x$with_tools" = xtrue])
+
+AC_DEFUN([FUNC_NOT_FOUND_TOOLS],
+       [ERR_NOT_FOUND([$1()], [tools])])
+
+AC_ARG_ENABLE([gpioset-interactive],
+       [AS_HELP_STRING([--enable-gpioset-interactive],
+               [enable gpioset interactive mode [default=no]])],
+       [if test "x$enableval" = xyes; then with_gpioset_interactive=true; fi],
+       [with_gpioset_interactive=false])
+AM_CONDITIONAL([WITH_GPIOSET_INTERACTIVE],
+       [test "x$with_gpioset_interactive" = xtrue])
+
+# FIXME Figure out why this AS_IF() cannot be dropped without causing the other
+# PKG_CHECK_MODULES() expansions fail to execute pkg-config.
+AS_IF([test "x$with_tools" = xtrue],
+       [# These are only needed to build tools
+       AC_CHECK_FUNC([daemon], [], [FUNC_NOT_FOUND_TOOLS([daemon])])
+       AC_CHECK_FUNC([asprintf], [], [FUNC_NOT_FOUND_LIB([asprintf])])
+       AC_CHECK_FUNC([scandir], [], [FUNC_NOT_FOUND_LIB([scandir])])
+       AC_CHECK_FUNC([versionsort], [], [FUNC_NOT_FOUND_LIB([versionsort])])
+       AS_IF([test "x$with_gpioset_interactive" = xtrue],
+               [PKG_CHECK_MODULES([LIBEDIT], [libedit >= 3.1])])
+       ])
+
+AC_ARG_ENABLE([tests],
+       [AS_HELP_STRING([--enable-tests],[enable libgpiod tests [default=no]])],
+       [if test "x$enableval" = xyes; then with_tests=true; fi],
+       [with_tests=false])
+AM_CONDITIONAL([WITH_TESTS], [test "x$with_tests" = xtrue])
+
+AC_ARG_ENABLE([profiling],
+       [AS_HELP_STRING([--enable-profiling],
+               [enable gcov profiling on the core library and tests [default=no]])],
+       [if test "x$enableval" = xyes; then with_profiling=true; fi],
+       [with_profiling=false])
+if test "x$with_profiling" = xtrue
+then
+       AC_SUBST(PROFILING_CFLAGS, ["-fprofile-arcs -ftest-coverage"])
+       AC_SUBST(PROFILING_LDFLAGS, ["-lgcov"])
+fi
+
+AC_DEFUN([FUNC_NOT_FOUND_TESTS],
+       [ERR_NOT_FOUND([$1()], [tests])])
+
+if test "x$with_tests" = xtrue
+then
+       # For libgpiosim
+       PKG_CHECK_MODULES([KMOD], [libkmod >= 18])
+       PKG_CHECK_MODULES([MOUNT], [mount >= 2.33.1])
+
+       # For core library tests
+       PKG_CHECK_MODULES([GLIB], [glib-2.0 >= 2.50])
+       PKG_CHECK_MODULES([GIO], [gio-2.0 >= 2.50])
+
+       AC_CHECK_FUNC([atexit], [], [FUNC_NOT_FOUND_LIB([atexit])])
+       AC_CHECK_FUNC([asprintf], [], [FUNC_NOT_FOUND_LIB([asprintf])])
+       AC_CHECK_FUNC([prctl], [], [FUNC_NOT_FOUND_LIB([prctl])])
+       AC_CHECK_FUNC([unlink], [], [FUNC_NOT_FOUND_LIB([unlink])])
+       AC_CHECK_FUNC([unlinkat], [], [FUNC_NOT_FOUND_LIB([unlinkat])])
+       AC_CHECK_FUNC([openat], [], [FUNC_NOT_FOUND_LIB([openat])])
+       AC_CHECK_FUNC([mkdirat], [], [FUNC_NOT_FOUND_LIB([mkdirat])])
+       AC_CHECK_FUNC([write], [], [FUNC_NOT_FOUND_LIB([write])])
+       AC_CHECK_FUNC([twalk], [], [FUNC_NOT_FOUND_LIB([twalk])])
+       AC_CHECK_FUNC([tsearch], [], [FUNC_NOT_FOUND_LIB([tsearch])])
+       AC_CHECK_FUNC([tdestroy], [], [FUNC_NOT_FOUND_LIB([tdestroy])])
+       AC_CHECK_FUNC([tdelete], [], [FUNC_NOT_FOUND_LIB([tdelete])])
+       AC_CHECK_HEADERS([sys/utsname.h], [], [HEADER_NOT_FOUND_LIB([sys/utsname.h])])
+       AC_CHECK_HEADERS([sys/mount.h], [], [HEADER_NOT_FOUND_LIB([sys/mount.h])])
+       AC_CHECK_HEADERS([sys/prctl.h], [], [HEADER_NOT_FOUND_LIB([sys/prctl.h])])
+       AC_CHECK_HEADERS([sys/random.h], [], [HEADER_NOT_FOUND_LIB([sys/random.h])])
+       AC_CHECK_HEADERS([linux/version.h], [], [HEADER_NOT_FOUND_LIB([linux/version.h])])
+       AC_CHECK_HEADERS([pthread.h], [], [HEADER_NOT_FOUND_LIB([pthread.h])])
+       AC_CHECK_LIB(pthread, pthread_mutex_lock, [], ERR_NOT_FOUND([pthread library], [tests]))
+
+       if test "x$with_tools" = xtrue
+       then
+               AC_CHECK_PROG([has_shunit2], [shunit2], [true], [false])
+               if test "x$has_shunit2" = "xfalse"
+               then
+                       AC_MSG_NOTICE([shunit2 not found - gpio-tools tests cannot be run])
+               fi
+       fi
+fi
+
+AC_ARG_ENABLE([examples],
+       [AS_HELP_STRING([--enable-examples], [enable building code examples[default=no]])],
+       [if test "x$enableval" = xyes; then with_examples=true; fi],
+       [with_examples=false])
+AM_CONDITIONAL([WITH_EXAMPLES], [test "x$with_examples" = xtrue])
+
+AC_ARG_ENABLE([bindings-cxx],
+       [AS_HELP_STRING([--enable-bindings-cxx],[enable C++ bindings [default=no]])],
+       [if test "x$enableval" = xyes; then with_bindings_cxx=true; fi],
+       [with_bindings_cxx=false])
+AM_CONDITIONAL([WITH_BINDINGS_CXX], [test "x$with_bindings_cxx" = xtrue])
+
+if test "x$with_bindings_cxx" = xtrue
+then
+       LT_LANG([C++])
+       # This needs autoconf-archive
+       AX_CXX_COMPILE_STDCXX_17([ext], [mandatory])
+
+       if test "x$with_tests" = xtrue
+       then
+               PKG_CHECK_MODULES([CATCH2], [catch2],, [
+                       AC_LANG_PUSH([C++])
+                       AC_CHECK_HEADERS([catch2/catch.hpp], [], [HEADER_NOT_FOUND_CXX([catch2/catch.hpp])])
+                       AC_LANG_POP([C++])
+               ])
+       fi
+fi
+
+AC_ARG_ENABLE([bindings-python],
+       [AS_HELP_STRING([--enable-bindings-python],[enable python3 bindings [default=no]])],
+       [if test "x$enableval" = xyes; then with_bindings_python=true; fi],
+       [with_bindings_python=false])
+AM_CONDITIONAL([WITH_BINDINGS_PYTHON], [test "x$with_bindings_python" = xtrue])
+
+if test "x$with_bindings_python" = xtrue
+then
+       AM_PATH_PYTHON([3.9], [],
+               [AC_MSG_ERROR([python3 not found - needed for python bindings])])
+       AC_CHECK_PROG([has_python_config], [python3-config], [true], [false])
+
+       if test "x$has_python_config" = xfalse
+       then
+               AC_MSG_ERROR([python3-config not found - needed for python bindings])
+       fi
+fi
+
+AC_ARG_ENABLE([bindings-rust],
+       [AS_HELP_STRING([--enable-bindings-rust],[enable rust bindings [default=no]])],
+       [if test "x$enableval" = xyes; then with_bindings_rust=true; fi],
+       [with_bindings_rust=false])
+AM_CONDITIONAL([WITH_BINDINGS_RUST], [test "x$with_bindings_rust" = xtrue])
+
+if test "x$with_bindings_rust" = xtrue
+then
+       AC_CHECK_PROG([has_cargo], [cargo], [true], [false])
+       if test "x$has_cargo" = xfalse
+       then
+               AC_MSG_ERROR([cargo not found - needed for rust bindings])
+       fi
+fi
+
+AC_CHECK_PROG([has_doxygen], [doxygen], [true], [false])
+AM_CONDITIONAL([HAS_DOXYGEN], [test "x$has_doxygen" = xtrue])
+if test "x$has_doxygen" = xfalse
+then
+       AC_MSG_NOTICE([doxygen not found - documentation cannot be generated])
+fi
+AM_COND_IF([HAS_DOXYGEN], [AC_CONFIG_FILES([Doxyfile])])
+
+if test "x$cross_compiling" = xno
+then
+       AC_CHECK_PROG([has_help2man], [help2man], [true], [false])
+fi
+AM_CONDITIONAL([WITH_MANPAGES], [test "x$has_help2man" = xtrue])
+if test "x$has_help2man" = xfalse
+then
+       AC_MSG_NOTICE([help2man not found - man pages cannot be generated automatically])
+fi
+
+AC_CONFIG_FILES([Makefile
+                include/Makefile
+                lib/Makefile
+                lib/libgpiod.pc
+                contrib/Makefile
+                examples/Makefile
+                tools/Makefile
+                tests/Makefile
+                tests/gpiosim/Makefile
+                bindings/cxx/libgpiodcxx.pc
+                bindings/Makefile
+                bindings/cxx/Makefile
+                bindings/cxx/gpiodcxx/Makefile
+                bindings/cxx/examples/Makefile
+                bindings/cxx/tests/Makefile
+                bindings/python/Makefile
+                bindings/python/gpiod/Makefile
+                bindings/python/gpiod/ext/Makefile
+                bindings/python/examples/Makefile
+                bindings/python/tests/Makefile
+                bindings/python/tests/gpiosim/Makefile
+                bindings/python/tests/procname/Makefile
+                bindings/rust/libgpiod-sys/src/Makefile
+                bindings/rust/libgpiod-sys/Makefile
+                bindings/rust/libgpiod/src/Makefile
+                bindings/rust/libgpiod/tests/common/Makefile
+                bindings/rust/libgpiod/tests/Makefile
+                bindings/rust/libgpiod/Makefile
+                bindings/rust/libgpiod/examples/Makefile
+                bindings/rust/Makefile
+                bindings/rust/gpiosim-sys/src/Makefile
+                bindings/rust/gpiosim-sys/Makefile
+                man/Makefile])
+
+AC_OUTPUT
diff --git a/contrib/Android.bp b/contrib/Android.bp
new file mode 100644 (file)
index 0000000..fbc2196
--- /dev/null
@@ -0,0 +1,136 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+// SPDX-FileCopyrightText: 2023 Benjamin Li <benl@squareup.com>
+
+// Instructions:
+// - Check out this repository as external/libgpiod.
+// - Move this build file to the project's root directory.
+
+//
+// libgpiod main library
+//
+
+cc_library {
+    name: "libgpiod",
+    defaults: [
+        "libgpiod_defaults",
+    ],
+    srcs: [
+        "lib/*.c",
+        "bindings/cxx/*.cpp",
+    ],
+    export_include_dirs: [
+        "include",
+        "bindings/cxx",
+    ],
+}
+
+cc_defaults {
+    name: "libgpiod_defaults",
+    device_specific: true,
+    cpp_std: "gnu++17",
+    cflags: [
+        // You may want to edit this with the version from configure.ac of
+        // the release you are using.
+        "-DGPIOD_VERSION_STR=\"unstable\"",
+    ],
+    cppflags: [
+        // Google C++ style is to not use exceptions, but this library does
+        // use them.
+        "-fexceptions",
+    ],
+    // Google C++ style is to not use runtime type information, but this
+    // library does use it.
+    rtti: true,
+}
+
+//
+// libgpiod tools
+//
+
+phony {
+    name: "libgpiod_tools",
+    required: [
+        "gpiodetect",
+        "gpioget",
+        "gpioinfo",
+        "gpiomon",
+        "gpionotify",
+        "gpioset",
+    ],
+}
+
+cc_binary {
+    name: "gpiodetect",
+    defaults: [
+        "libgpiod_defaults",
+        "libgpiod_tools_defaults",
+    ],
+    srcs: [
+        "tools/gpiodetect.c",
+    ],
+}
+
+cc_binary {
+    name: "gpioget",
+    defaults: [
+        "libgpiod_defaults",
+        "libgpiod_tools_defaults",
+    ],
+    srcs: [
+        "tools/gpioget.c",
+    ],
+}
+
+cc_binary {
+    name: "gpioinfo",
+    defaults: [
+        "libgpiod_defaults",
+        "libgpiod_tools_defaults",
+    ],
+    srcs: [
+        "tools/gpioinfo.c",
+    ],
+}
+
+cc_binary {
+    name: "gpiomon",
+    defaults: [
+        "libgpiod_defaults",
+        "libgpiod_tools_defaults",
+    ],
+    srcs: [
+        "tools/gpiomon.c",
+    ],
+}
+
+cc_binary {
+    name: "gpionotify",
+    defaults: [
+        "libgpiod_defaults",
+        "libgpiod_tools_defaults",
+    ],
+    srcs: [
+        "tools/gpionotify.c",
+    ],
+}
+
+cc_binary {
+    name: "gpioset",
+    defaults: [
+        "libgpiod_defaults",
+        "libgpiod_tools_defaults",
+    ],
+    srcs: [
+        "tools/gpioset.c",
+    ],
+}
+
+cc_defaults {
+    name: "libgpiod_tools_defaults",
+    srcs: [
+        "tools/tools-common.c",
+    ],
+    shared_libs: [
+        "libgpiod",
+    ],
+}
diff --git a/contrib/Makefile.am b/contrib/Makefile.am
new file mode 100644 (file)
index 0000000..1b50e86
--- /dev/null
@@ -0,0 +1,4 @@
+# SPDX-License-Identifier: GPL-2.0-or-later
+# SPDX-FileCopyrightText: 2023 Bartosz Golaszewski <bartosz.golaszewski@linaro.org>
+
+EXTRA_DIST = Android.bp
diff --git a/examples/.gitignore b/examples/.gitignore
new file mode 100644 (file)
index 0000000..8fd3ff3
--- /dev/null
@@ -0,0 +1,16 @@
+# SPDX-License-Identifier: GPL-2.0-or-later
+# SPDX-FileCopyrightText: 2023 Kent Gibson <warthog618@gmail.com>
+
+async_watch_line_value
+find_line_by_name
+get_chip_info
+get_line_info
+get_line_value
+get_multiple_line_values
+reconfigure_input_to_output
+toggle_line_value
+toggle_multiple_line_values
+watch_line_info
+watch_line_rising
+watch_line_value
+watch_multiple_line_values
diff --git a/examples/Makefile.am b/examples/Makefile.am
new file mode 100644 (file)
index 0000000..ed01dbc
--- /dev/null
@@ -0,0 +1,22 @@
+# SPDX-License-Identifier: GPL-2.0-or-later
+# SPDX-FileCopyrightText: 2023 Kent Gibson <warthog618@gmail.com>
+
+AM_CFLAGS = -I$(top_srcdir)/include/ -include $(top_builddir)/config.h
+AM_CFLAGS += -Wall -Wextra -g -std=gnu89
+
+LDADD = $(top_builddir)/lib/libgpiod.la
+
+noinst_PROGRAMS = \
+       async_watch_line_value \
+       find_line_by_name \
+       get_chip_info \
+       get_line_info \
+       get_line_value \
+       get_multiple_line_values \
+       reconfigure_input_to_output \
+       toggle_line_value \
+       toggle_multiple_line_values \
+       watch_line_info \
+       watch_line_rising \
+       watch_line_value \
+       watch_multiple_line_values
diff --git a/examples/async_watch_line_value.c b/examples/async_watch_line_value.c
new file mode 100644 (file)
index 0000000..8b1d643
--- /dev/null
@@ -0,0 +1,143 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+// SPDX-FileCopyrightText: 2023 Kent Gibson <warthog618@gmail.com>
+
+/* Minimal example of asynchronously watching for edges on a single line. */
+
+#include <errno.h>
+#include <gpiod.h>
+#include <poll.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+/* Request a line as input with edge detection. */
+static struct gpiod_line_request *request_input_line(const char *chip_path,
+                                                    unsigned int offset,
+                                                    const char *consumer)
+{
+       struct gpiod_request_config *req_cfg = NULL;
+       struct gpiod_line_request *request = NULL;
+       struct gpiod_line_settings *settings;
+       struct gpiod_line_config *line_cfg;
+       struct gpiod_chip *chip;
+       int ret;
+
+       chip = gpiod_chip_open(chip_path);
+       if (!chip)
+               return NULL;
+
+       settings = gpiod_line_settings_new();
+       if (!settings)
+               goto close_chip;
+
+       gpiod_line_settings_set_direction(settings, GPIOD_LINE_DIRECTION_INPUT);
+       gpiod_line_settings_set_edge_detection(settings, GPIOD_LINE_EDGE_BOTH);
+       /* Assume a button connecting the pin to ground, so pull it up... */
+       gpiod_line_settings_set_bias(settings, GPIOD_LINE_BIAS_PULL_UP);
+       /* ... and provide some debounce. */
+       gpiod_line_settings_set_debounce_period_us(settings, 10000);
+
+       line_cfg = gpiod_line_config_new();
+       if (!line_cfg)
+               goto free_settings;
+
+       ret = gpiod_line_config_add_line_settings(line_cfg, &offset, 1,
+                                                 settings);
+       if (ret)
+               goto free_line_config;
+
+       if (consumer) {
+               req_cfg = gpiod_request_config_new();
+               if (!req_cfg)
+                       goto free_line_config;
+
+               gpiod_request_config_set_consumer(req_cfg, consumer);
+       }
+
+       request = gpiod_chip_request_lines(chip, req_cfg, line_cfg);
+
+       gpiod_request_config_free(req_cfg);
+
+free_line_config:
+       gpiod_line_config_free(line_cfg);
+
+free_settings:
+       gpiod_line_settings_free(settings);
+
+close_chip:
+       gpiod_chip_close(chip);
+
+       return request;
+}
+
+static const char *edge_event_type_str(struct gpiod_edge_event *event)
+{
+       switch (gpiod_edge_event_get_event_type(event)) {
+       case GPIOD_EDGE_EVENT_RISING_EDGE:
+               return "Rising";
+       case GPIOD_EDGE_EVENT_FALLING_EDGE:
+               return "Falling";
+       default:
+               return "Unknown";
+       }
+}
+
+int main(void)
+{
+       /* Example configuration - customize to suit your situation. */
+       static const char *const chip_path = "/dev/gpiochip0";
+       static const unsigned int line_offset = 5;
+
+       struct gpiod_edge_event_buffer *event_buffer;
+       struct gpiod_line_request *request;
+       struct gpiod_edge_event *event;
+       int i, ret, event_buf_size;
+       struct pollfd pollfd;
+
+       request = request_input_line(chip_path, line_offset,
+                                    "async-watch-line-value");
+       if (!request) {
+               fprintf(stderr, "failed to request line: %s\n",
+                       strerror(errno));
+               return EXIT_FAILURE;
+       }
+
+       /*
+        * A larger buffer is an optimisation for reading bursts of events from
+        * the kernel, but that is not necessary in this case, so 1 is fine.
+        */
+       event_buf_size = 1;
+       event_buffer = gpiod_edge_event_buffer_new(event_buf_size);
+       if (!event_buffer) {
+               fprintf(stderr, "failed to create event buffer: %s\n",
+                       strerror(errno));
+               return EXIT_FAILURE;
+       }
+
+       pollfd.fd = gpiod_line_request_get_fd(request);
+       pollfd.events = POLLIN;
+
+       for (;;) {
+               ret = poll(&pollfd, 1, -1);
+               if (ret == -1) {
+                       fprintf(stderr, "error waiting for edge events: %s\n",
+                               strerror(errno));
+                       return EXIT_FAILURE;
+               }
+               ret = gpiod_line_request_read_edge_events(request, event_buffer,
+                                                         event_buf_size);
+               if (ret == -1) {
+                       fprintf(stderr, "error reading edge events: %s\n",
+                               strerror(errno));
+                       return EXIT_FAILURE;
+               }
+               for (i = 0; i < ret; i++) {
+                       event = gpiod_edge_event_buffer_get_event(event_buffer,
+                                                                 i);
+                       printf("offset: %d  type: %-7s  event #%ld\n",
+                              gpiod_edge_event_get_line_offset(event),
+                              edge_event_type_str(event),
+                              gpiod_edge_event_get_line_seqno(event));
+               }
+       }
+}
diff --git a/examples/find_line_by_name.c b/examples/find_line_by_name.c
new file mode 100644 (file)
index 0000000..346d258
--- /dev/null
@@ -0,0 +1,105 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+// SPDX-FileCopyrightText: 2023 Kent Gibson <warthog618@gmail.com>
+
+/* Minimal example of finding a line with the given name. */
+
+#include <dirent.h>
+#include <errno.h>
+#include <gpiod.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/stat.h>
+
+static int chip_dir_filter(const struct dirent *entry)
+{
+       struct stat sb;
+       int ret = 0;
+       char *path;
+
+       if (asprintf(&path, "/dev/%s", entry->d_name) < 0)
+               return 0;
+
+       if ((lstat(path, &sb) == 0) && (!S_ISLNK(sb.st_mode)) &&
+           gpiod_is_gpiochip_device(path))
+               ret = 1;
+
+       free(path);
+
+       return ret;
+}
+
+static int all_chip_paths(char ***paths_ptr)
+{
+       int i, j, num_chips, ret = 0;
+       struct dirent **entries;
+       char **paths;
+
+       num_chips = scandir("/dev/", &entries, chip_dir_filter, versionsort);
+       if (num_chips < 0)
+               return 0;
+
+       paths = calloc(num_chips, sizeof(*paths));
+       if (!paths)
+               return 0;
+
+       for (i = 0; i < num_chips; i++) {
+               if (asprintf(&paths[i], "/dev/%s", entries[i]->d_name) < 0) {
+                       for (j = 0; j < i; j++)
+                               free(paths[j]);
+
+                       free(paths);
+                       return 0;
+               }
+       }
+
+       *paths_ptr = paths;
+       ret = num_chips;
+
+       for (i = 0; i < num_chips; i++)
+               free(entries[i]);
+
+       free(entries);
+       return ret;
+}
+
+int main(void)
+{
+       /* Example configuration - customize to suit your situation. */
+       static const char *const line_name = "GPIO19";
+
+       struct gpiod_chip_info *cinfo;
+       int i, num_chips, offset;
+       struct gpiod_chip *chip;
+       char **chip_paths;
+
+       /*
+        * Names are not guaranteed unique, so this finds the first line with
+        * the given name.
+        */
+       num_chips = all_chip_paths(&chip_paths);
+       for (i = 0; i < num_chips; i++) {
+               chip = gpiod_chip_open(chip_paths[i]);
+               if (!chip)
+                       continue;
+
+               offset = gpiod_chip_get_line_offset_from_name(chip, line_name);
+               if (offset == -1)
+                       goto close_chip;
+
+               cinfo = gpiod_chip_get_info(chip);
+               if (!cinfo)
+                       goto close_chip;
+
+               printf("%s: %s %d\n", line_name,
+                      gpiod_chip_info_get_name(cinfo), offset);
+
+               return EXIT_SUCCESS;
+
+close_chip:
+               gpiod_chip_close(chip);
+       }
+
+       printf("line '%s' not found\n", line_name);
+       return EXIT_FAILURE;
+}
diff --git a/examples/get_chip_info.c b/examples/get_chip_info.c
new file mode 100644 (file)
index 0000000..5c181c8
--- /dev/null
@@ -0,0 +1,40 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+// SPDX-FileCopyrightText: 2023 Kent Gibson <warthog618@gmail.com>
+
+/* Minimal example of reading the info for a chip. */
+
+#include <errno.h>
+#include <gpiod.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+int main(void)
+{
+       /* Example configuration - customize to suit your situation. */
+       static const char *const chip_path = "/dev/gpiochip0";
+
+       struct gpiod_chip_info *info;
+       struct gpiod_chip *chip;
+
+       chip = gpiod_chip_open(chip_path);
+       if (!chip) {
+               fprintf(stderr, "failed to open chip: %s\n", strerror(errno));
+               return EXIT_FAILURE;
+       }
+
+       info = gpiod_chip_get_info(chip);
+       if (!info) {
+               fprintf(stderr, "failed to read info: %s\n", strerror(errno));
+               return EXIT_FAILURE;
+       }
+
+       printf("%s [%s] (%zu lines)\n", gpiod_chip_info_get_name(info),
+              gpiod_chip_info_get_label(info),
+              gpiod_chip_info_get_num_lines(info));
+
+       gpiod_chip_info_free(info);
+       gpiod_chip_close(chip);
+
+       return EXIT_SUCCESS;
+}
diff --git a/examples/get_line_info.c b/examples/get_line_info.c
new file mode 100644 (file)
index 0000000..ad28686
--- /dev/null
@@ -0,0 +1,56 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+// SPDX-FileCopyrightText: 2023 Kent Gibson <warthog618@gmail.com>
+
+/* Minimal example of reading the info for a line. */
+
+#include <errno.h>
+#include <gpiod.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+int main(void)
+{
+       /* Example configuration - customize to suit your situation. */
+       static const char *const chip_path = "/dev/gpiochip0";
+       static const unsigned int line_offset = 3;
+
+       const char *name, *consumer, *dir;
+       struct gpiod_line_info *info;
+       struct gpiod_chip *chip;
+
+       chip = gpiod_chip_open(chip_path);
+       if (!chip) {
+               fprintf(stderr, "failed to open chip: %s\n", strerror(errno));
+               return EXIT_FAILURE;
+       }
+
+       info = gpiod_chip_get_line_info(chip, line_offset);
+       if (!info) {
+               fprintf(stderr, "failed to read info: %s\n", strerror(errno));
+               return EXIT_FAILURE;
+       }
+
+       name = gpiod_line_info_get_name(info);
+       if (!name)
+               name = "unnamed";
+
+       consumer = gpiod_line_info_get_consumer(info);
+       if (!consumer)
+               consumer = "unused";
+
+       dir = (gpiod_line_info_get_direction(info) ==
+              GPIOD_LINE_DIRECTION_INPUT) ?
+                     "input" :
+                     "output";
+
+       printf("line %3d: %12s %12s %8s %10s\n",
+              gpiod_line_info_get_offset(info), name, consumer, dir,
+              gpiod_line_info_is_active_low(info) ? "active-low" :
+                                                    "active-high");
+
+       gpiod_line_info_free(info);
+       gpiod_chip_close(chip);
+
+       return EXIT_SUCCESS;
+}
diff --git a/examples/get_line_value.c b/examples/get_line_value.c
new file mode 100644 (file)
index 0000000..1de9901
--- /dev/null
@@ -0,0 +1,106 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+// SPDX-FileCopyrightText: 2023 Kent Gibson <warthog618@gmail.com>
+
+/* Minimal example of reading a single line. */
+
+#include <errno.h>
+#include <gpiod.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+/* Request a line as input. */
+static struct gpiod_line_request *request_input_line(const char *chip_path,
+                                                    unsigned int offset,
+                                                    const char *consumer)
+{
+       struct gpiod_request_config *req_cfg = NULL;
+       struct gpiod_line_request *request = NULL;
+       struct gpiod_line_settings *settings;
+       struct gpiod_line_config *line_cfg;
+       struct gpiod_chip *chip;
+       int ret;
+
+       chip = gpiod_chip_open(chip_path);
+       if (!chip)
+               return NULL;
+
+       settings = gpiod_line_settings_new();
+       if (!settings)
+               goto close_chip;
+
+       gpiod_line_settings_set_direction(settings, GPIOD_LINE_DIRECTION_INPUT);
+
+       line_cfg = gpiod_line_config_new();
+       if (!line_cfg)
+               goto free_settings;
+
+       ret = gpiod_line_config_add_line_settings(line_cfg, &offset, 1,
+                                                 settings);
+       if (ret)
+               goto free_line_config;
+
+       if (consumer) {
+               req_cfg = gpiod_request_config_new();
+               if (!req_cfg)
+                       goto free_line_config;
+
+               gpiod_request_config_set_consumer(req_cfg, consumer);
+       }
+
+       request = gpiod_chip_request_lines(chip, req_cfg, line_cfg);
+
+       gpiod_request_config_free(req_cfg);
+
+free_line_config:
+       gpiod_line_config_free(line_cfg);
+
+free_settings:
+       gpiod_line_settings_free(settings);
+
+close_chip:
+       gpiod_chip_close(chip);
+
+       return request;
+}
+
+static int print_value(unsigned int offset, enum gpiod_line_value value)
+{
+       if (value == GPIOD_LINE_VALUE_ACTIVE)
+               printf("%d=Active\n", offset);
+       else if (value == GPIOD_LINE_VALUE_INACTIVE) {
+               printf("%d=Inactive\n", offset);
+       } else {
+               fprintf(stderr, "error reading value: %s\n",
+                       strerror(errno));
+               return EXIT_FAILURE;
+       }
+
+       return EXIT_SUCCESS;
+}
+
+int main(void)
+{
+       /* Example configuration - customize to suit your situation. */
+       static const char *const chip_path = "/dev/gpiochip0";
+       static const unsigned int line_offset = 5;
+
+       struct gpiod_line_request *request;
+       enum gpiod_line_value value;
+       int ret;
+
+       request = request_input_line(chip_path, line_offset, "get-line-value");
+       if (!request) {
+               fprintf(stderr, "failed to request line: %s\n",
+                       strerror(errno));
+               return EXIT_FAILURE;
+       }
+
+       value = gpiod_line_request_get_value(request, line_offset);
+       ret = print_value(line_offset, value);
+
+       /* not strictly required here, but if the app wasn't exiting... */
+       gpiod_line_request_release(request);
+
+       return ret;
+}
diff --git a/examples/get_multiple_line_values.c b/examples/get_multiple_line_values.c
new file mode 100644 (file)
index 0000000..c6df3f6
--- /dev/null
@@ -0,0 +1,119 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+// SPDX-FileCopyrightText: 2023 Kent Gibson <warthog618@gmail.com>
+
+/* Minimal example of reading multiple lines. */
+
+#include <errno.h>
+#include <gpiod.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#define NUM_LINES 3
+
+/* Request a line as input. */
+static struct gpiod_line_request *
+request_input_lines(const char *chip_path, const unsigned int *offsets,
+                   unsigned int num_lines, const char *consumer)
+{
+       struct gpiod_request_config *req_cfg = NULL;
+       struct gpiod_line_request *request = NULL;
+       struct gpiod_line_settings *settings;
+       struct gpiod_line_config *line_cfg;
+       struct gpiod_chip *chip;
+       unsigned int i;
+       int ret;
+
+       chip = gpiod_chip_open(chip_path);
+       if (!chip)
+               return NULL;
+
+       settings = gpiod_line_settings_new();
+       if (!settings)
+               goto close_chip;
+
+       gpiod_line_settings_set_direction(settings, GPIOD_LINE_DIRECTION_INPUT);
+
+       line_cfg = gpiod_line_config_new();
+       if (!line_cfg)
+               goto free_settings;
+
+       for (i = 0; i < num_lines; i++) {
+               ret = gpiod_line_config_add_line_settings(line_cfg, &offsets[i],
+                                                         1, settings);
+               if (ret)
+                       goto free_line_config;
+       }
+
+       if (consumer) {
+               req_cfg = gpiod_request_config_new();
+               if (!req_cfg)
+                       goto free_line_config;
+
+               gpiod_request_config_set_consumer(req_cfg, consumer);
+       }
+
+       request = gpiod_chip_request_lines(chip, req_cfg, line_cfg);
+
+       gpiod_request_config_free(req_cfg);
+
+free_line_config:
+       gpiod_line_config_free(line_cfg);
+
+free_settings:
+       gpiod_line_settings_free(settings);
+
+close_chip:
+       gpiod_chip_close(chip);
+
+       return request;
+}
+
+static int print_values(const unsigned int *offsets, unsigned int num_lines,
+                       enum gpiod_line_value *values)
+{
+       unsigned int i;
+
+       for (i = 0; i < num_lines; i++) {
+               if (values[i] == GPIOD_LINE_VALUE_ACTIVE)
+                       printf("%d=Active ", offsets[i]);
+               else if (values[i] == GPIOD_LINE_VALUE_INACTIVE) {
+                       printf("%d=Inactive ", offsets[i]);
+               } else {
+                       fprintf(stderr, "error reading value: %s\n",
+                               strerror(errno));
+                       return EXIT_FAILURE;
+               }
+       }
+
+       printf("\n");
+
+       return EXIT_SUCCESS;
+}
+
+int main(void)
+{
+       /* Example configuration - customize to suit your situation. */
+       static const char *const chip_path = "/dev/gpiochip0";
+       static const unsigned int line_offsets[NUM_LINES] = { 5, 3, 7 };
+
+       enum gpiod_line_value values[NUM_LINES];
+       struct gpiod_line_request *request;
+       int ret;
+
+       request = request_input_lines(chip_path, line_offsets, NUM_LINES,
+                                     "get-multiple-line-values");
+       if (!request) {
+               fprintf(stderr, "failed to request lines: %s\n",
+                       strerror(errno));
+               return EXIT_FAILURE;
+       }
+
+       ret = gpiod_line_request_get_values(request, values);
+       if (ret == -1) {
+               fprintf(stderr, "failed to get values: %s\n", strerror(errno));
+               return EXIT_FAILURE;
+       }
+
+       return print_values(line_offsets, NUM_LINES, values);
+}
diff --git a/examples/reconfigure_input_to_output.c b/examples/reconfigure_input_to_output.c
new file mode 100644 (file)
index 0000000..451bb0e
--- /dev/null
@@ -0,0 +1,155 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+// SPDX-FileCopyrightText: 2023 Kent Gibson <warthog618@gmail.com>
+
+/*
+ * Example of a bi-directional line requested as input and then switched
+ * to output.
+ */
+
+#include <errno.h>
+#include <gpiod.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+/* Request a line as input. */
+static struct gpiod_line_request *request_input_line(const char *chip_path,
+                                                    unsigned int offset,
+                                                    const char *consumer)
+{
+       struct gpiod_request_config *req_cfg = NULL;
+       struct gpiod_line_request *request = NULL;
+       struct gpiod_line_settings *settings;
+       struct gpiod_line_config *line_cfg;
+       struct gpiod_chip *chip;
+       int ret;
+
+       chip = gpiod_chip_open(chip_path);
+       if (!chip)
+               return NULL;
+
+       settings = gpiod_line_settings_new();
+       if (!settings)
+               goto close_chip;
+
+       gpiod_line_settings_set_direction(settings, GPIOD_LINE_DIRECTION_INPUT);
+
+       line_cfg = gpiod_line_config_new();
+       if (!line_cfg)
+               goto free_settings;
+
+       ret = gpiod_line_config_add_line_settings(line_cfg, &offset, 1,
+                                                 settings);
+       if (ret)
+               goto free_line_config;
+
+       if (consumer) {
+               req_cfg = gpiod_request_config_new();
+               if (!req_cfg)
+                       goto free_line_config;
+
+               gpiod_request_config_set_consumer(req_cfg, consumer);
+       }
+
+       request = gpiod_chip_request_lines(chip, req_cfg, line_cfg);
+
+       gpiod_request_config_free(req_cfg);
+
+free_line_config:
+       gpiod_line_config_free(line_cfg);
+
+free_settings:
+       gpiod_line_settings_free(settings);
+
+close_chip:
+       gpiod_chip_close(chip);
+
+       return request;
+}
+
+static int reconfigure_as_output_line(struct gpiod_line_request *request,
+                                     unsigned int offset,
+                                     enum gpiod_line_value value)
+{
+       struct gpiod_request_config *req_cfg = NULL;
+       struct gpiod_line_settings *settings;
+       struct gpiod_line_config *line_cfg;
+       int ret = -1;
+
+       settings = gpiod_line_settings_new();
+       if (!settings)
+               return -1;
+
+       gpiod_line_settings_set_direction(settings,
+                                         GPIOD_LINE_DIRECTION_OUTPUT);
+       gpiod_line_settings_set_output_value(settings, value);
+
+       line_cfg = gpiod_line_config_new();
+       if (!line_cfg)
+               goto free_settings;
+
+       ret = gpiod_line_config_add_line_settings(line_cfg, &offset, 1,
+                                                 settings);
+       if (ret)
+               goto free_line_config;
+
+       ret = gpiod_line_request_reconfigure_lines(request, line_cfg);
+
+       gpiod_request_config_free(req_cfg);
+
+free_line_config:
+       gpiod_line_config_free(line_cfg);
+
+free_settings:
+       gpiod_line_settings_free(settings);
+
+       return ret;
+}
+
+static const char * value_str(enum gpiod_line_value value)
+{
+       if (value == GPIOD_LINE_VALUE_ACTIVE)
+               return "Active";
+       else if (value == GPIOD_LINE_VALUE_INACTIVE) {
+               return "Inactive";
+       } else {
+               return "Unknown";
+       }
+}
+
+int main(void)
+{
+       /* Example configuration - customize to suit your situation. */
+       static const char *const chip_path = "/dev/gpiochip0";
+       static const unsigned int line_offset = 5;
+
+       struct gpiod_line_request *request;
+       enum gpiod_line_value value;
+       int ret;
+
+       /* request the line initially as an input */
+       request = request_input_line(chip_path, line_offset,
+                                    "reconfigure-input-to-output");
+       if (!request) {
+               fprintf(stderr, "failed to request line: %s\n",
+                       strerror(errno));
+               return EXIT_FAILURE;
+       }
+
+       /* read the current line value */
+       value = gpiod_line_request_get_value(request, line_offset);
+       printf("%d=%s (input)\n", line_offset, value_str(value));
+
+       /* switch the line to an output and drive it low */
+       ret = reconfigure_as_output_line(request, line_offset,
+                                        GPIOD_LINE_VALUE_INACTIVE);
+
+       /* report the current driven value */
+       value = gpiod_line_request_get_value(request, line_offset);
+       printf("%d=%s (output)\n", line_offset, value_str(value));
+
+       /* not strictly required here, but if the app wasn't exiting... */
+       gpiod_line_request_release(request);
+
+       return ret;
+}
diff --git a/examples/toggle_line_value.c b/examples/toggle_line_value.c
new file mode 100644 (file)
index 0000000..6e522d6
--- /dev/null
@@ -0,0 +1,113 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+// SPDX-FileCopyrightText: 2023 Kent Gibson <warthog618@gmail.com>
+
+/* Minimal example of toggling a single line. */
+
+#include <errno.h>
+#include <gpiod.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+static struct gpiod_line_request *
+request_output_line(const char *chip_path, unsigned int offset,
+                   enum gpiod_line_value value, const char *consumer)
+{
+       struct gpiod_request_config *req_cfg = NULL;
+       struct gpiod_line_request *request = NULL;
+       struct gpiod_line_settings *settings;
+       struct gpiod_line_config *line_cfg;
+       struct gpiod_chip *chip;
+       int ret;
+
+       chip = gpiod_chip_open(chip_path);
+       if (!chip)
+               return NULL;
+
+       settings = gpiod_line_settings_new();
+       if (!settings)
+               goto close_chip;
+
+       gpiod_line_settings_set_direction(settings,
+                                         GPIOD_LINE_DIRECTION_OUTPUT);
+       gpiod_line_settings_set_output_value(settings, value);
+
+       line_cfg = gpiod_line_config_new();
+       if (!line_cfg)
+               goto free_settings;
+
+       ret = gpiod_line_config_add_line_settings(line_cfg, &offset, 1,
+                                                 settings);
+       if (ret)
+               goto free_line_config;
+
+       if (consumer) {
+               req_cfg = gpiod_request_config_new();
+               if (!req_cfg)
+                       goto free_line_config;
+
+               gpiod_request_config_set_consumer(req_cfg, consumer);
+       }
+
+       request = gpiod_chip_request_lines(chip, req_cfg, line_cfg);
+
+       gpiod_request_config_free(req_cfg);
+
+free_line_config:
+       gpiod_line_config_free(line_cfg);
+
+free_settings:
+       gpiod_line_settings_free(settings);
+
+close_chip:
+       gpiod_chip_close(chip);
+
+       return request;
+}
+
+static enum gpiod_line_value toggle_line_value(enum gpiod_line_value value)
+{
+       return (value == GPIOD_LINE_VALUE_ACTIVE) ? GPIOD_LINE_VALUE_INACTIVE :
+                                                   GPIOD_LINE_VALUE_ACTIVE;
+}
+
+static const char * value_str(enum gpiod_line_value value)
+{
+       if (value == GPIOD_LINE_VALUE_ACTIVE)
+               return "Active";
+       else if (value == GPIOD_LINE_VALUE_INACTIVE) {
+               return "Inactive";
+       } else {
+               return "Unknown";
+       }
+}
+
+int main(void)
+{
+       /* Example configuration - customize to suit your situation. */
+       static const char *const chip_path = "/dev/gpiochip0";
+       static const unsigned int line_offset = 5;
+
+       enum gpiod_line_value value = GPIOD_LINE_VALUE_ACTIVE;
+       struct gpiod_line_request *request;
+
+       request = request_output_line(chip_path, line_offset, value,
+                                     "toggle-line-value");
+       if (!request) {
+               fprintf(stderr, "failed to request line: %s\n",
+                       strerror(errno));
+               return EXIT_FAILURE;
+       }
+
+       for (;;) {
+               printf("%d=%s\n", line_offset, value_str(value));
+               sleep(1);
+               value = toggle_line_value(value);
+               gpiod_line_request_set_value(request, line_offset, value);
+       }
+
+       gpiod_line_request_release(request);
+
+       return EXIT_SUCCESS;
+}
diff --git a/examples/toggle_multiple_line_values.c b/examples/toggle_multiple_line_values.c
new file mode 100644 (file)
index 0000000..824d5ea
--- /dev/null
@@ -0,0 +1,136 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+// SPDX-FileCopyrightText: 2023 Kent Gibson <warthog618@gmail.com>
+
+/* Minimal example of toggling multiple lines. */
+
+#include <errno.h>
+#include <gpiod.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+#define NUM_LINES 3
+
+static struct gpiod_line_request *
+request_output_lines(const char *chip_path, const unsigned int *offsets,
+                    enum gpiod_line_value *values, unsigned int num_lines,
+                    const char *consumer)
+{
+       struct gpiod_request_config *rconfig = NULL;
+       struct gpiod_line_request *request = NULL;
+       struct gpiod_line_settings *settings;
+       struct gpiod_line_config *lconfig;
+       struct gpiod_chip *chip;
+       unsigned int i;
+       int ret;
+
+       chip = gpiod_chip_open(chip_path);
+       if (!chip)
+               return NULL;
+
+       settings = gpiod_line_settings_new();
+       if (!settings)
+               goto close_chip;
+
+       gpiod_line_settings_set_direction(settings,
+                                         GPIOD_LINE_DIRECTION_OUTPUT);
+
+       lconfig = gpiod_line_config_new();
+       if (!lconfig)
+               goto free_settings;
+
+       for (i = 0; i < num_lines; i++) {
+               ret = gpiod_line_config_add_line_settings(lconfig, &offsets[i],
+                                                         1, settings);
+               if (ret)
+                       goto free_line_config;
+       }
+       gpiod_line_config_set_output_values(lconfig, values, num_lines);
+
+       if (consumer) {
+               rconfig = gpiod_request_config_new();
+               if (!rconfig)
+                       goto free_line_config;
+
+               gpiod_request_config_set_consumer(rconfig, consumer);
+       }
+
+       request = gpiod_chip_request_lines(chip, rconfig, lconfig);
+
+       gpiod_request_config_free(rconfig);
+
+free_line_config:
+       gpiod_line_config_free(lconfig);
+
+free_settings:
+       gpiod_line_settings_free(settings);
+
+close_chip:
+       gpiod_chip_close(chip);
+
+       return request;
+}
+
+static enum gpiod_line_value toggle_line_value(enum gpiod_line_value value)
+{
+       return (value == GPIOD_LINE_VALUE_ACTIVE) ? GPIOD_LINE_VALUE_INACTIVE :
+                                                   GPIOD_LINE_VALUE_ACTIVE;
+}
+
+static void toggle_line_values(enum gpiod_line_value *values,
+                              unsigned int num_lines)
+{
+       unsigned int i;
+
+       for (i = 0; i < num_lines; i++)
+               values[i] = toggle_line_value(values[i]);
+}
+
+static void print_values(const unsigned int *offsets,
+                        const enum gpiod_line_value *values,
+                        unsigned int num_lines)
+{
+       unsigned int i;
+
+       for (i = 0; i < num_lines; i++) {
+               if (values[i] == GPIOD_LINE_VALUE_ACTIVE)
+                       printf("%d=Active ", offsets[i]);
+               else
+                       printf("%d=Inactive ", offsets[i]);
+       }
+
+       printf("\n");
+}
+
+int main(void)
+{
+       /* Example configuration - customize to suit your situation. */
+       static const char *const chip_path = "/dev/gpiochip0";
+       static const unsigned int line_offsets[NUM_LINES] = { 5, 3, 7 };
+
+       enum gpiod_line_value values[NUM_LINES] = { GPIOD_LINE_VALUE_ACTIVE,
+                                                   GPIOD_LINE_VALUE_ACTIVE,
+                                                   GPIOD_LINE_VALUE_INACTIVE };
+       struct gpiod_line_request *request;
+
+       request = request_output_lines(chip_path, line_offsets, values,
+                                      NUM_LINES,
+                                      "toggle-multiple-line-values");
+       if (!request) {
+               fprintf(stderr, "failed to request line: %s\n",
+                       strerror(errno));
+               return EXIT_FAILURE;
+       }
+
+       for (;;) {
+               print_values(line_offsets, values, NUM_LINES);
+               sleep(1);
+               toggle_line_values(values, NUM_LINES);
+               gpiod_line_request_set_values(request, values);
+       }
+
+       gpiod_line_request_release(request);
+
+       return EXIT_SUCCESS;
+}
diff --git a/examples/watch_line_info.c b/examples/watch_line_info.c
new file mode 100644 (file)
index 0000000..9df3121
--- /dev/null
@@ -0,0 +1,73 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+// SPDX-FileCopyrightText: 2023 Kent Gibson <warthog618@gmail.com>
+
+/* Minimal example of watching for info changes on particular lines. */
+
+#include <errno.h>
+#include <gpiod.h>
+#include <inttypes.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#define NUM_LINES 3
+
+static const char *event_type(struct gpiod_info_event *event)
+{
+       switch (gpiod_info_event_get_event_type(event)) {
+       case GPIOD_INFO_EVENT_LINE_REQUESTED:
+               return "Requested";
+       case GPIOD_INFO_EVENT_LINE_RELEASED:
+               return "Released";
+       case GPIOD_INFO_EVENT_LINE_CONFIG_CHANGED:
+               return "Reconfig";
+       default:
+               return "Unknown";
+       }
+}
+
+int main(void)
+{
+       /* Example configuration - customize to suit your situation. */
+       static const char *const chip_path = "/dev/gpiochip0";
+       static const unsigned int line_offsets[NUM_LINES] = { 5, 3, 7 };
+
+       struct gpiod_info_event *event;
+       struct gpiod_line_info *info;
+       struct gpiod_chip *chip;
+       uint64_t timestamp_ns;
+       unsigned int i;
+
+       chip = gpiod_chip_open(chip_path);
+       if (!chip) {
+               fprintf(stderr, "failed to open chip: %s\n", strerror(errno));
+               return EXIT_FAILURE;
+       }
+
+       for (i = 0; i < NUM_LINES; i++) {
+               info = gpiod_chip_watch_line_info(chip, line_offsets[i]);
+               if (!info) {
+                       fprintf(stderr, "failed to read info: %s\n",
+                               strerror(errno));
+                       return EXIT_FAILURE;
+               }
+       }
+
+       for (;;) {
+               /* Blocks until an event is available. */
+               event = gpiod_chip_read_info_event(chip);
+               if (!event) {
+                       fprintf(stderr, "failed to read event: %s\n",
+                               strerror(errno));
+                       return EXIT_FAILURE;
+               }
+
+               info = gpiod_info_event_get_line_info(event);
+               timestamp_ns = gpiod_info_event_get_timestamp_ns(event);
+               printf("line %3d: %-9s %" PRIu64 ".%" PRIu64 "\n",
+                      gpiod_line_info_get_offset(info), event_type(event),
+                      timestamp_ns / 1000000000, timestamp_ns % 1000000000);
+
+               gpiod_info_event_free(event);
+       }
+}
diff --git a/examples/watch_line_rising.c b/examples/watch_line_rising.c
new file mode 100644 (file)
index 0000000..062a46a
--- /dev/null
@@ -0,0 +1,129 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+// SPDX-FileCopyrightText: 2023 Kent Gibson <warthog618@gmail.com>
+
+/* Minimal example of watching for rising edges on a single line. */
+
+#include <errno.h>
+#include <gpiod.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+/* Request a line as input with edge detection. */
+static struct gpiod_line_request *request_input_line(const char *chip_path,
+                                                    unsigned int offset,
+                                                    const char *consumer)
+{
+       struct gpiod_request_config *req_cfg = NULL;
+       struct gpiod_line_request *request = NULL;
+       struct gpiod_line_settings *settings;
+       struct gpiod_line_config *line_cfg;
+       struct gpiod_chip *chip;
+       int ret;
+
+       chip = gpiod_chip_open(chip_path);
+       if (!chip)
+               return NULL;
+
+       settings = gpiod_line_settings_new();
+       if (!settings)
+               goto close_chip;
+
+       gpiod_line_settings_set_direction(settings, GPIOD_LINE_DIRECTION_INPUT);
+       gpiod_line_settings_set_edge_detection(settings, GPIOD_LINE_EDGE_RISING);
+
+       line_cfg = gpiod_line_config_new();
+       if (!line_cfg)
+               goto free_settings;
+
+       ret = gpiod_line_config_add_line_settings(line_cfg, &offset, 1,
+                                                 settings);
+       if (ret)
+               goto free_line_config;
+
+       if (consumer) {
+               req_cfg = gpiod_request_config_new();
+               if (!req_cfg)
+                       goto free_line_config;
+
+               gpiod_request_config_set_consumer(req_cfg, consumer);
+       }
+
+       request = gpiod_chip_request_lines(chip, req_cfg, line_cfg);
+
+       gpiod_request_config_free(req_cfg);
+
+free_line_config:
+       gpiod_line_config_free(line_cfg);
+
+free_settings:
+       gpiod_line_settings_free(settings);
+
+close_chip:
+       gpiod_chip_close(chip);
+
+       return request;
+}
+
+static const char *edge_event_type_str(struct gpiod_edge_event *event)
+{
+       switch (gpiod_edge_event_get_event_type(event)) {
+       case GPIOD_EDGE_EVENT_RISING_EDGE:
+               return "Rising";
+       case GPIOD_EDGE_EVENT_FALLING_EDGE:
+               return "Falling";
+       default:
+               return "Unknown";
+       }
+}
+
+int main(void)
+{
+       /* Example configuration - customize to suit your situation. */
+       static const char *const chip_path = "/dev/gpiochip0";
+       static const unsigned int line_offset = 5;
+
+       struct gpiod_edge_event_buffer *event_buffer;
+       struct gpiod_line_request *request;
+       struct gpiod_edge_event *event;
+       int i, ret, event_buf_size;
+
+       request = request_input_line(chip_path, line_offset,
+                                    "watch-line-value");
+       if (!request) {
+               fprintf(stderr, "failed to request line: %s\n",
+                       strerror(errno));
+               return EXIT_FAILURE;
+       }
+
+       /*
+        * A larger buffer is an optimisation for reading bursts of events from
+        * the kernel, but that is not necessary in this case, so 1 is fine.
+        */
+       event_buf_size = 1;
+       event_buffer = gpiod_edge_event_buffer_new(event_buf_size);
+       if (!event_buffer) {
+               fprintf(stderr, "failed to create event buffer: %s\n",
+                       strerror(errno));
+               return EXIT_FAILURE;
+       }
+
+       for (;;) {
+               /* Blocks until at least one event is available. */
+               ret = gpiod_line_request_read_edge_events(request, event_buffer,
+                                                         event_buf_size);
+               if (ret == -1) {
+                       fprintf(stderr, "error reading edge events: %s\n",
+                               strerror(errno));
+                       return EXIT_FAILURE;
+               }
+               for (i = 0; i < ret; i++) {
+                       event = gpiod_edge_event_buffer_get_event(event_buffer,
+                                                                 i);
+                       printf("offset: %d  type: %-7s  event #%ld\n",
+                              gpiod_edge_event_get_line_offset(event),
+                              edge_event_type_str(event),
+                              gpiod_edge_event_get_line_seqno(event));
+               }
+       }
+}
diff --git a/examples/watch_line_value.c b/examples/watch_line_value.c
new file mode 100644 (file)
index 0000000..879b09b
--- /dev/null
@@ -0,0 +1,133 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+// SPDX-FileCopyrightText: 2023 Kent Gibson <warthog618@gmail.com>
+
+/* Minimal example of watching for edges on a single line. */
+
+#include <errno.h>
+#include <gpiod.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+/* Request a line as input with edge detection. */
+static struct gpiod_line_request *request_input_line(const char *chip_path,
+                                                    unsigned int offset,
+                                                    const char *consumer)
+{
+       struct gpiod_request_config *req_cfg = NULL;
+       struct gpiod_line_request *request = NULL;
+       struct gpiod_line_settings *settings;
+       struct gpiod_line_config *line_cfg;
+       struct gpiod_chip *chip;
+       int ret;
+
+       chip = gpiod_chip_open(chip_path);
+       if (!chip)
+               return NULL;
+
+       settings = gpiod_line_settings_new();
+       if (!settings)
+               goto close_chip;
+
+       gpiod_line_settings_set_direction(settings, GPIOD_LINE_DIRECTION_INPUT);
+       gpiod_line_settings_set_edge_detection(settings, GPIOD_LINE_EDGE_BOTH);
+       /* Assume a button connecting the pin to ground, so pull it up... */
+       gpiod_line_settings_set_bias(settings, GPIOD_LINE_BIAS_PULL_UP);
+       /* ... and provide some debounce. */
+       gpiod_line_settings_set_debounce_period_us(settings, 10000);
+
+       line_cfg = gpiod_line_config_new();
+       if (!line_cfg)
+               goto free_settings;
+
+       ret = gpiod_line_config_add_line_settings(line_cfg, &offset, 1,
+                                                 settings);
+       if (ret)
+               goto free_line_config;
+
+       if (consumer) {
+               req_cfg = gpiod_request_config_new();
+               if (!req_cfg)
+                       goto free_line_config;
+
+               gpiod_request_config_set_consumer(req_cfg, consumer);
+       }
+
+       request = gpiod_chip_request_lines(chip, req_cfg, line_cfg);
+
+       gpiod_request_config_free(req_cfg);
+
+free_line_config:
+       gpiod_line_config_free(line_cfg);
+
+free_settings:
+       gpiod_line_settings_free(settings);
+
+close_chip:
+       gpiod_chip_close(chip);
+
+       return request;
+}
+
+static const char *edge_event_type_str(struct gpiod_edge_event *event)
+{
+       switch (gpiod_edge_event_get_event_type(event)) {
+       case GPIOD_EDGE_EVENT_RISING_EDGE:
+               return "Rising";
+       case GPIOD_EDGE_EVENT_FALLING_EDGE:
+               return "Falling";
+       default:
+               return "Unknown";
+       }
+}
+
+int main(void)
+{
+       /* Example configuration - customize to suit your situation. */
+       static const char *const chip_path = "/dev/gpiochip0";
+       static const unsigned int line_offset = 5;
+
+       struct gpiod_edge_event_buffer *event_buffer;
+       struct gpiod_line_request *request;
+       struct gpiod_edge_event *event;
+       int i, ret, event_buf_size;
+
+       request = request_input_line(chip_path, line_offset,
+                                    "watch-line-value");
+       if (!request) {
+               fprintf(stderr, "failed to request line: %s\n",
+                       strerror(errno));
+               return EXIT_FAILURE;
+       }
+
+       /*
+        * A larger buffer is an optimisation for reading bursts of events from
+        * the kernel, but that is not necessary in this case, so 1 is fine.
+        */
+       event_buf_size = 1;
+       event_buffer = gpiod_edge_event_buffer_new(event_buf_size);
+       if (!event_buffer) {
+               fprintf(stderr, "failed to create event buffer: %s\n",
+                       strerror(errno));
+               return EXIT_FAILURE;
+       }
+
+       for (;;) {
+               /* Blocks until at least one event is available. */
+               ret = gpiod_line_request_read_edge_events(request, event_buffer,
+                                                         event_buf_size);
+               if (ret == -1) {
+                       fprintf(stderr, "error reading edge events: %s\n",
+                               strerror(errno));
+                       return EXIT_FAILURE;
+               }
+               for (i = 0; i < ret; i++) {
+                       event = gpiod_edge_event_buffer_get_event(event_buffer,
+                                                                 i);
+                       printf("offset: %d  type: %-7s  event #%ld\n",
+                              gpiod_edge_event_get_line_offset(event),
+                              edge_event_type_str(event),
+                              gpiod_edge_event_get_line_seqno(event));
+               }
+       }
+}
diff --git a/examples/watch_multiple_line_values.c b/examples/watch_multiple_line_values.c
new file mode 100644 (file)
index 0000000..e955b2c
--- /dev/null
@@ -0,0 +1,140 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+// SPDX-FileCopyrightText: 2023 Kent Gibson <warthog618@gmail.com>
+
+/* Minimal example of watching for edges on multiple lines. */
+
+#include <errno.h>
+#include <gpiod.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#define NUM_LINES 3
+
+/* Request a line as input with edge detection. */
+static struct gpiod_line_request *
+request_input_lines(const char *chip_path, const unsigned int *offsets,
+                   unsigned int num_lines, const char *consumer)
+{
+       struct gpiod_request_config *req_cfg = NULL;
+       struct gpiod_line_request *request = NULL;
+       struct gpiod_line_settings *settings;
+       struct gpiod_line_config *line_cfg;
+       struct gpiod_chip *chip;
+       unsigned int i;
+       int ret;
+
+       chip = gpiod_chip_open(chip_path);
+       if (!chip)
+               return NULL;
+
+       settings = gpiod_line_settings_new();
+       if (!settings)
+               goto close_chip;
+
+       gpiod_line_settings_set_direction(settings, GPIOD_LINE_DIRECTION_INPUT);
+       gpiod_line_settings_set_edge_detection(settings, GPIOD_LINE_EDGE_BOTH);
+       /* Assume a button connecting the pin to ground, so pull it up... */
+       gpiod_line_settings_set_bias(settings, GPIOD_LINE_BIAS_PULL_UP);
+       /* ... and provide some debounce. */
+       gpiod_line_settings_set_debounce_period_us(settings, 10000);
+
+       line_cfg = gpiod_line_config_new();
+       if (!line_cfg)
+               goto free_settings;
+
+       for (i = 0; i < num_lines; i++) {
+               ret = gpiod_line_config_add_line_settings(line_cfg, &offsets[i],
+                                                         1, settings);
+               if (ret)
+                       goto free_line_config;
+       }
+
+       if (consumer) {
+               req_cfg = gpiod_request_config_new();
+               if (!req_cfg)
+                       goto free_line_config;
+
+               gpiod_request_config_set_consumer(req_cfg, consumer);
+       }
+
+       request = gpiod_chip_request_lines(chip, req_cfg, line_cfg);
+
+       gpiod_request_config_free(req_cfg);
+
+free_line_config:
+       gpiod_line_config_free(line_cfg);
+
+free_settings:
+       gpiod_line_settings_free(settings);
+
+close_chip:
+       gpiod_chip_close(chip);
+
+       return request;
+}
+
+static const char *edge_event_type_str(struct gpiod_edge_event *event)
+{
+       switch (gpiod_edge_event_get_event_type(event)) {
+       case GPIOD_EDGE_EVENT_RISING_EDGE:
+               return "Rising";
+       case GPIOD_EDGE_EVENT_FALLING_EDGE:
+               return "Falling";
+       default:
+               return "Unknown";
+       }
+}
+
+int main(void)
+{
+       /* Example configuration - customize to suit your situation. */
+       static const char *const chip_path = "/dev/gpiochip0";
+       static const unsigned int line_offsets[NUM_LINES] = { 5, 3, 7 };
+
+       struct gpiod_edge_event_buffer *event_buffer;
+       struct gpiod_line_request *request;
+       struct gpiod_edge_event *event;
+       int i, ret, event_buf_size;
+
+       request = request_input_lines(chip_path, line_offsets, NUM_LINES,
+                                     "watch-multiple-line-values");
+       if (!request) {
+               fprintf(stderr, "failed to request line: %s\n",
+                       strerror(errno));
+               return EXIT_FAILURE;
+       }
+
+       /*
+        * A larger buffer is an optimisation for reading bursts of events from
+        * the kernel, so even a value of 1 would be fine.
+        * The size here allows for a simultaneous event on each of the lines
+        * to be copied in one read.
+        */
+       event_buf_size = NUM_LINES;
+       event_buffer = gpiod_edge_event_buffer_new(event_buf_size);
+       if (!event_buffer) {
+               fprintf(stderr, "failed to create event buffer: %s\n",
+                       strerror(errno));
+               return EXIT_FAILURE;
+       }
+
+       for (;;) {
+               /* Blocks until at least one event is available. */
+               ret = gpiod_line_request_read_edge_events(request, event_buffer,
+                                                         event_buf_size);
+               if (ret == -1) {
+                       fprintf(stderr, "error reading edge events: %s\n",
+                               strerror(errno));
+                       return EXIT_FAILURE;
+               }
+               for (i = 0; i < ret; i++) {
+                       event = gpiod_edge_event_buffer_get_event(event_buffer,
+                                                                 i);
+                       printf("offset: %d  type: %-7s  event #%ld\n",
+                              gpiod_edge_event_get_line_offset(event),
+                              edge_event_type_str(event),
+                              gpiod_edge_event_get_line_seqno(event));
+               }
+       }
+}
diff --git a/include/Makefile.am b/include/Makefile.am
new file mode 100644 (file)
index 0000000..7f986ec
--- /dev/null
@@ -0,0 +1,4 @@
+# SPDX-License-Identifier: GPL-2.0-or-later
+# SPDX-FileCopyrightText: 2017-2021 Bartosz Golaszewski <bartekgola@gmail.com>
+
+include_HEADERS = gpiod.h
diff --git a/include/gpiod.h b/include/gpiod.h
new file mode 100644 (file)
index 0000000..d86c6ac
--- /dev/null
@@ -0,0 +1,1366 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/* SPDX-FileCopyrightText: 2017-2022 Bartosz Golaszewski <brgl@bgdev.pl> */
+
+/**
+ * @file gpiod.h
+ */
+
+#ifndef __LIBGPIOD_GPIOD_H__
+#define __LIBGPIOD_GPIOD_H__
+
+#include <stdbool.h>
+#include <stddef.h>
+#include <stdint.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * @mainpage libgpiod public API
+ *
+ * This is the complete documentation of the public API made available to
+ * users of libgpiod.
+ *
+ * The API is logically split into several sections. For each opaque data
+ * class, there's a set of functions for manipulating it. Together they can be
+ * thought of as objects and their methods in OOP parlance.
+ *
+ * General note on error handling: all functions exported by libgpiod that
+ * can fail, set errno to one of the error values defined in errno.h upon
+ * failure. The way of notifying the caller that an error occurred varies
+ * between functions, but in general a function that returns an int, returns -1
+ * on error, while a function returning a pointer indicates an error condition
+ * by returning a NULL pointer. It's not practical to list all possible error
+ * codes for every function as they propagate errors from the underlying libc
+ * functions.
+ *
+ * In general libgpiod functions are NULL-aware. For functions that are
+ * logically methods of data classes - ones that take a pointer to the object
+ * of that class as the first argument - passing a NULL pointer will result in
+ * the program aborting the execution. For non-methods, init functions and
+ * methods that take a pointer as any of the subsequent arguments, the handling
+ * of a NULL-pointer depends on the implementation and may range from gracefully
+ * handling it, ignoring it or returning an error.
+ *
+ * libgpiod is thread-aware but does not provide any further thread-safety
+ * guarantees. This requires the user to ensure that at most one thread may
+ * work with an object at any time. Sharing objects across threads is allowed
+ * if a suitable synchronization mechanism serializes the access. Different,
+ * standalone objects can safely be used concurrently. Most libgpiod objects
+ * are standalone. Exceptions - such as events allocated in buffers - exist and
+ * are noted in the documentation.
+ */
+
+/**
+ * @struct gpiod_chip
+ * @{
+ *
+ * Refer to @ref chips for functions that operate on gpiod_chip.
+ *
+ * @}
+*/
+struct gpiod_chip;
+
+/**
+ * @struct gpiod_chip_info
+ * @{
+ *
+ * Refer to @ref chip_info for functions that operate on gpiod_chip_info.
+ *
+ * @}
+*/
+struct gpiod_chip_info;
+
+/**
+ * @struct gpiod_line_info
+ * @{
+ *
+ * Refer to @ref line_info for functions that operate on gpiod_line_info.
+ *
+ * @}
+*/
+struct gpiod_line_info;
+
+/**
+ * @struct gpiod_line_settings
+ * @{
+ *
+ * Refer to @ref line_settings for functions that operate on
+ * gpiod_line_settings.
+ *
+ * @}
+*/
+struct gpiod_line_settings;
+
+/**
+ * @struct gpiod_line_config
+ * @{
+ *
+ * Refer to @ref line_config for functions that operate on gpiod_line_config.
+ *
+ * @}
+*/
+struct gpiod_line_config;
+
+/**
+ * @struct gpiod_request_config
+ * @{
+ *
+ * Refer to @ref request_config for functions that operate on
+ * gpiod_request_config.
+ *
+ * @}
+*/
+struct gpiod_request_config;
+
+/**
+ * @struct gpiod_line_request
+ * @{
+ *
+ * Refer to @ref line_request for functions that operate on
+ * gpiod_line_request.
+ *
+ * @}
+*/
+struct gpiod_line_request;
+
+/**
+ * @struct gpiod_info_event
+ * @{
+ *
+ * Refer to @ref line_watch for functions that operate on gpiod_info_event.
+ *
+ * @}
+*/
+struct gpiod_info_event;
+
+/**
+ * @struct gpiod_edge_event
+ * @{
+ *
+ * Refer to @ref edge_event for functions that operate on gpiod_edge_event.
+ *
+ * @}
+*/
+struct gpiod_edge_event;
+
+/**
+ * @struct gpiod_edge_event_buffer
+ * @{
+ *
+ * Refer to @ref edge_event for functions that operate on
+ * gpiod_edge_event_buffer.
+ *
+ * @}
+*/
+struct gpiod_edge_event_buffer;
+
+/**
+ * @defgroup chips GPIO chips
+ * @{
+ *
+ * Functions and data structures for GPIO chip operations.
+ *
+ * A GPIO chip object is associated with an open file descriptor to the GPIO
+ * character device. It exposes basic information about the chip and allows
+ * callers to retrieve information about each line, watch lines for state
+ * changes and make line requests.
+ */
+
+/**
+ * @brief Open a chip by path.
+ * @param path Path to the gpiochip device file.
+ * @return GPIO chip object or NULL if an error occurred. The returned object
+ *         must be closed by the caller using ::gpiod_chip_close.
+ */
+struct gpiod_chip *gpiod_chip_open(const char *path);
+
+/**
+ * @brief Close the chip and release all associated resources.
+ * @param chip Chip to close.
+ */
+void gpiod_chip_close(struct gpiod_chip *chip);
+
+/**
+ * @brief Get information about the chip.
+ * @param chip GPIO chip object.
+ * @return New GPIO chip info object or NULL if an error occurred. The returned
+ *         object must be freed by the caller using ::gpiod_chip_info_free.
+ */
+struct gpiod_chip_info *gpiod_chip_get_info(struct gpiod_chip *chip);
+
+/**
+ * @brief Get the path used to open the chip.
+ * @param chip GPIO chip object.
+ * @return Path to the file passed as argument to ::gpiod_chip_open. The
+ *         returned pointer is valid for the lifetime of the chip object and
+ *         must not be freed by the caller.
+ */
+const char *gpiod_chip_get_path(struct gpiod_chip *chip);
+
+/**
+ * @brief Get a snapshot of information about a line.
+ * @param chip GPIO chip object.
+ * @param offset The offset of the GPIO line.
+ * @return New GPIO line info object or NULL if an error occurred. The returned
+ *         object must be freed by the caller using ::gpiod_line_info_free.
+ */
+struct gpiod_line_info *gpiod_chip_get_line_info(struct gpiod_chip *chip,
+                                                unsigned int offset);
+
+/**
+ * @brief Get a snapshot of the status of a line and start watching it for
+ *        future changes.
+ * @param chip GPIO chip object.
+ * @param offset The offset of the GPIO line.
+ * @return New GPIO line info object or NULL if an error occurred. The returned
+ *         object must be freed by the caller using ::gpiod_line_info_free.
+ * @note Line status does not include the line value. To monitor the line
+ *       value the line must be requested as an input with edge detection set.
+ */
+struct gpiod_line_info *gpiod_chip_watch_line_info(struct gpiod_chip *chip,
+                                                  unsigned int offset);
+
+/**
+ * @brief Stop watching a line for status changes.
+ * @param chip GPIO chip object.
+ * @param offset The offset of the line to stop watching.
+ * @return 0 on success, -1 on failure.
+ */
+int gpiod_chip_unwatch_line_info(struct gpiod_chip *chip, unsigned int offset);
+
+/**
+ * @brief Get the file descriptor associated with the chip.
+ * @param chip GPIO chip object.
+ * @return File descriptor number for the chip.
+ *
+ * This function never fails. The returned file descriptor must not be closed
+ * by the caller. Call ::gpiod_chip_close to close the file descriptor by
+ * closing the chip owning it.
+ */
+int gpiod_chip_get_fd(struct gpiod_chip *chip);
+
+/**
+ * @brief Wait for line status change events on any of the watched lines
+ *        on the chip.
+ * @param chip GPIO chip object.
+ * @param timeout_ns Wait time limit in nanoseconds. If set to 0, the function
+ *                   returns immediatelly. If set to a negative number, the
+ *                   function blocks indefinitely until an event becomes
+ *                   available.
+ * @return 0 if wait timed out, -1 if an error occurred, 1 if an event is
+ *         pending.
+ */
+int gpiod_chip_wait_info_event(struct gpiod_chip *chip, int64_t timeout_ns);
+
+/**
+ * @brief Read a single line status change event from the chip.
+ * @param chip GPIO chip object.
+ * @return Newly read watch event object or NULL on error. The event must be
+ *         freed by the caller using ::gpiod_info_event_free.
+ * @note If no events are pending, this function will block.
+ */
+struct gpiod_info_event *gpiod_chip_read_info_event(struct gpiod_chip *chip);
+
+/**
+ * @brief Map a line's name to its offset within the chip.
+ * @param chip GPIO chip object.
+ * @param name Name of the GPIO line to map.
+ * @return Offset of the line within the chip or -1 on error.
+ * @note If a line with given name is not exposed by the chip, the function
+ *       sets errno to ENOENT.
+ */
+int gpiod_chip_get_line_offset_from_name(struct gpiod_chip *chip,
+                                        const char *name);
+
+/**
+ * @brief Request a set of lines for exclusive usage.
+ * @param chip GPIO chip object.
+ * @param req_cfg Request config object. Can be NULL for default settings.
+ * @param line_cfg Line config object.
+ * @return New line request object or NULL if an error occurred. The request
+ *         must be released by the caller using ::gpiod_line_request_release.
+ */
+struct gpiod_line_request *
+gpiod_chip_request_lines(struct gpiod_chip *chip,
+                        struct gpiod_request_config *req_cfg,
+                        struct gpiod_line_config *line_cfg);
+
+/**
+ * @}
+ *
+ * @defgroup chip_info Chip info
+ * @{
+ *
+ * Functions for retrieving kernel information about chips.
+ *
+ * Line info object contains an immutable snapshot of a chip's status.
+ *
+ * The chip info contains all the publicly available information about a
+ * chip.
+ *
+ * Some accessor methods return pointers. Those pointers refer to internal
+ * fields. The lifetimes of those fields are tied to the lifetime of the
+ * containing chip info object. Such pointers remain valid until
+ * ::gpiod_chip_info_free is called on the containing chip info object. They
+ * must not be freed by the caller.
+ */
+
+/**
+ * @brief Free a chip info object and release all associated resources.
+ * @param info GPIO chip info object to free.
+ */
+void gpiod_chip_info_free(struct gpiod_chip_info *info);
+
+/**
+ * @brief Get the name of the chip as represented in the kernel.
+ * @param info GPIO chip info object.
+ * @return Valid pointer to a human-readable string containing the chip name.
+ *         The string lifetime is tied to the chip info object so the pointer
+ *         must not be freed by the caller.
+ */
+const char *gpiod_chip_info_get_name(struct gpiod_chip_info *info);
+
+/**
+ * @brief Get the label of the chip as represented in the kernel.
+ * @param info GPIO chip info object.
+ * @return Valid pointer to a human-readable string containing the chip label.
+ *         The string lifetime is tied to the chip info object so the pointer
+ *         must not be freed by the caller.
+ */
+const char *gpiod_chip_info_get_label(struct gpiod_chip_info *info);
+
+/**
+ * @brief Get the number of lines exposed by the chip.
+ * @param info GPIO chip info object.
+ * @return Number of GPIO lines.
+ */
+size_t gpiod_chip_info_get_num_lines(struct gpiod_chip_info *info);
+
+/**
+ * @}
+ *
+ * @defgroup line_defs Line definitions
+ * @{
+ *
+ * These defines are used across the API.
+ */
+
+/**
+ * @brief Logical line state.
+ */
+enum gpiod_line_value {
+       GPIOD_LINE_VALUE_ERROR = -1,
+       /**< Returned to indicate an error when reading the value. */
+       GPIOD_LINE_VALUE_INACTIVE = 0,
+       /**< Line is logically inactive. */
+       GPIOD_LINE_VALUE_ACTIVE = 1,
+       /**< Line is logically active. */
+};
+
+/**
+ * @brief Direction settings.
+ */
+enum gpiod_line_direction {
+       GPIOD_LINE_DIRECTION_AS_IS = 1,
+       /**< Request the line(s), but don't change direction. */
+       GPIOD_LINE_DIRECTION_INPUT,
+       /**< Direction is input - for reading the value of an externally driven
+        *   GPIO line. */
+       GPIOD_LINE_DIRECTION_OUTPUT,
+       /**< Direction is output - for driving the GPIO line. */
+};
+
+/**
+ * @brief Edge detection settings.
+ */
+enum gpiod_line_edge {
+       GPIOD_LINE_EDGE_NONE = 1,
+       /**< Line edge detection is disabled. */
+       GPIOD_LINE_EDGE_RISING,
+       /**< Line detects rising edge events. */
+       GPIOD_LINE_EDGE_FALLING,
+       /**< Line detects falling edge events. */
+       GPIOD_LINE_EDGE_BOTH,
+       /**< Line detects both rising and falling edge events. */
+};
+
+/**
+ * @brief Internal bias settings.
+ */
+enum gpiod_line_bias {
+       GPIOD_LINE_BIAS_AS_IS = 1,
+       /**< Don't change the bias setting when applying line config. */
+       GPIOD_LINE_BIAS_UNKNOWN,
+       /**< The internal bias state is unknown. */
+       GPIOD_LINE_BIAS_DISABLED,
+       /**< The internal bias is disabled. */
+       GPIOD_LINE_BIAS_PULL_UP,
+       /**< The internal pull-up bias is enabled. */
+       GPIOD_LINE_BIAS_PULL_DOWN,
+       /**< The internal pull-down bias is enabled. */
+};
+
+/**
+ * @brief Drive settings.
+ */
+enum gpiod_line_drive {
+       GPIOD_LINE_DRIVE_PUSH_PULL = 1,
+       /**< Drive setting is push-pull. */
+       GPIOD_LINE_DRIVE_OPEN_DRAIN,
+       /**< Line output is open-drain. */
+       GPIOD_LINE_DRIVE_OPEN_SOURCE,
+       /**< Line output is open-source. */
+};
+
+/**
+ * @brief Clock settings.
+ */
+enum gpiod_line_clock {
+       GPIOD_LINE_CLOCK_MONOTONIC = 1,
+       /**< Line uses the monotonic clock for edge event timestamps. */
+       GPIOD_LINE_CLOCK_REALTIME,
+       /**< Line uses the realtime clock for edge event timestamps. */
+       GPIOD_LINE_CLOCK_HTE,
+       /**< Line uses the hardware timestamp engine for event timestamps. */
+};
+
+/**
+ * @}
+ *
+ * @defgroup line_info Line info
+ * @{
+ *
+ * Functions for retrieving kernel information about both requested and free
+ * lines.
+ *
+ * Line info object contains an immutable snapshot of a line's status.
+ *
+ * The line info contains all the publicly available information about a
+ * line, which does not include the line value. The line must be requested
+ * to access the line value.
+ *
+ * Some accessor methods return pointers. Those pointers refer to internal
+ * fields. The lifetimes of those fields are tied to the lifetime of the
+ * containing line info object. Such pointers remain valid until
+ * ::gpiod_line_info_free is called on the containing line info object. They
+ * must not be freed by the caller.
+ */
+
+/**
+ * @brief Free a line info object and release all associated resources.
+ * @param info GPIO line info object to free.
+ */
+void gpiod_line_info_free(struct gpiod_line_info *info);
+
+/**
+ * @brief Copy a line info object.
+ * @param info Line info to copy.
+ * @return Copy of the line info or NULL on error. The returned object must
+ *         be freed by the caller using :gpiod_line_info_free.
+ */
+struct gpiod_line_info *gpiod_line_info_copy(struct gpiod_line_info *info);
+
+/**
+ * @brief Get the offset of the line.
+ * @param info GPIO line info object.
+ * @return Offset of the line within the parent chip.
+ *
+ * The offset uniquely identifies the line on the chip. The combination of the
+ * chip and offset uniquely identifies the line within the system.
+ */
+unsigned int gpiod_line_info_get_offset(struct gpiod_line_info *info);
+
+/**
+ * @brief Get the name of the line.
+ * @param info GPIO line info object.
+ * @return Name of the GPIO line as it is represented in the kernel.
+ *         This function returns a valid pointer to a null-terminated string
+ *         or NULL if the line is unnamed. The string lifetime is tied to the
+ *         line info object so the pointer must not be freed.
+ */
+const char *gpiod_line_info_get_name(struct gpiod_line_info *info);
+
+/**
+ * @brief Check if the line is in use.
+ * @param info GPIO line object.
+ * @return True if the line is in use, false otherwise.
+ *
+ * The exact reason a line is busy cannot be determined from user space.
+ * It may have been requested by another process or hogged by the kernel.
+ * It only matters that the line is used and can't be requested until
+ * released by the existing consumer.
+ */
+bool gpiod_line_info_is_used(struct gpiod_line_info *info);
+
+/**
+ * @brief Get the name of the consumer of the line.
+ * @param info GPIO line info object.
+ * @return Name of the GPIO consumer as it is represented in the kernel.
+ *        This function returns a valid pointer to a null-terminated string
+ *        or NULL if the consumer name is not set.
+ *        The string lifetime is tied to the line info object so the pointer
+ *        must not be freed.
+ */
+const char *gpiod_line_info_get_consumer(struct gpiod_line_info *info);
+
+/**
+ * @brief Get the direction setting of the line.
+ * @param info GPIO line info object.
+ * @return Returns ::GPIOD_LINE_DIRECTION_INPUT or
+ *        ::GPIOD_LINE_DIRECTION_OUTPUT.
+ */
+enum gpiod_line_direction
+gpiod_line_info_get_direction(struct gpiod_line_info *info);
+
+/**
+ * @brief Get the edge detection setting of the line.
+ * @param info GPIO line info object.
+ * @return Returns ::GPIOD_LINE_EDGE_NONE, ::GPIOD_LINE_EDGE_RISING,
+ *        ::GPIOD_LINE_EDGE_FALLING or ::GPIOD_LINE_EDGE_BOTH.
+ */
+enum gpiod_line_edge
+gpiod_line_info_get_edge_detection(struct gpiod_line_info *info);
+
+/**
+ * @brief Get the bias setting of the line.
+ * @param info GPIO line object.
+ * @return Returns ::GPIOD_LINE_BIAS_PULL_UP, ::GPIOD_LINE_BIAS_PULL_DOWN,
+ *         ::GPIOD_LINE_BIAS_DISABLED or ::GPIOD_LINE_BIAS_UNKNOWN.
+ */
+enum gpiod_line_bias
+gpiod_line_info_get_bias(struct gpiod_line_info *info);
+
+/**
+ * @brief Get the drive setting of the line.
+ * @param info GPIO line info object.
+ * @return Returns ::GPIOD_LINE_DRIVE_PUSH_PULL, ::GPIOD_LINE_DRIVE_OPEN_DRAIN
+ *         or ::GPIOD_LINE_DRIVE_OPEN_SOURCE.
+ */
+enum gpiod_line_drive
+gpiod_line_info_get_drive(struct gpiod_line_info *info);
+
+/**
+ * @brief Check if the logical value of the line is inverted compared to the
+ *        physical.
+ * @param info GPIO line object.
+ * @return True if the line is "active-low", false otherwise.
+ */
+bool gpiod_line_info_is_active_low(struct gpiod_line_info *info);
+
+/**
+ * @brief Check if the line is debounced (either by hardware or by the kernel
+ *        software debouncer).
+ * @param info GPIO line info object.
+ * @return True if the line is debounced, false otherwise.
+ */
+bool gpiod_line_info_is_debounced(struct gpiod_line_info *info);
+
+/**
+ * @brief Get the debounce period of the line, in microseconds.
+ * @param info GPIO line info object.
+ * @return Debounce period in microseconds.
+ *         0 if the line is not debounced.
+ */
+unsigned long
+gpiod_line_info_get_debounce_period_us(struct gpiod_line_info *info);
+
+/**
+ * @brief Get the event clock setting used for edge event timestamps for the
+ *        line.
+ * @param info GPIO line info object.
+ * @return Returns ::GPIOD_LINE_CLOCK_MONOTONIC, ::GPIOD_LINE_CLOCK_HTE or
+ *         ::GPIOD_LINE_CLOCK_REALTIME.
+ */
+enum gpiod_line_clock
+gpiod_line_info_get_event_clock(struct gpiod_line_info *info);
+
+/**
+ * @}
+ *
+ * @defgroup line_watch Line status watch events
+ * @{
+ *
+ * Accessors for the info event objects allowing to monitor changes in GPIO
+ * line status.
+ *
+ * Callers are notified about changes in a line's status due to GPIO uAPI
+ * calls. Each info event contains information about the event itself
+ * (timestamp, type) as well as a snapshot of line's status in the form
+ * of a line-info object.
+ */
+
+/**
+ * @brief Line status change event types.
+ */
+enum gpiod_info_event_type {
+       GPIOD_INFO_EVENT_LINE_REQUESTED = 1,
+       /**< Line has been requested. */
+       GPIOD_INFO_EVENT_LINE_RELEASED,
+       /**< Previously requested line has been released. */
+       GPIOD_INFO_EVENT_LINE_CONFIG_CHANGED,
+       /**< Line configuration has changed. */
+};
+
+/**
+ * @brief Free the info event object and release all associated resources.
+ * @param event Info event to free.
+ */
+void gpiod_info_event_free(struct gpiod_info_event *event);
+
+/**
+ * @brief Get the event type of the status change event.
+ * @param event Line status watch event.
+ * @return One of ::GPIOD_INFO_EVENT_LINE_REQUESTED,
+ *         ::GPIOD_INFO_EVENT_LINE_RELEASED or
+ *         ::GPIOD_INFO_EVENT_LINE_CONFIG_CHANGED.
+ */
+enum gpiod_info_event_type
+gpiod_info_event_get_event_type(struct gpiod_info_event *event);
+
+/**
+ * @brief Get the timestamp of the event.
+ * @param event Line status watch event.
+ * @return Timestamp in nanoseconds, read from the monotonic clock.
+ */
+uint64_t gpiod_info_event_get_timestamp_ns(struct gpiod_info_event *event);
+
+/**
+ * @brief Get the snapshot of line-info associated with the event.
+ * @param event Line info event object.
+ * @return Returns a pointer to the line-info object associated with the event.
+ *         The object lifetime is tied to the event object, so the pointer must
+ *         be not be freed by the caller.
+ * @warning Thread-safety:
+ *          Since the line-info object is tied to the event, different threads
+ *          may not operate on the event and line-info at the same time. The
+ *          line-info can be copied using ::gpiod_line_info_copy in order to
+ *          create a standalone object - which then may safely be used from a
+ *          different thread concurrently.
+ */
+struct gpiod_line_info *
+gpiod_info_event_get_line_info(struct gpiod_info_event *event);
+
+/**
+ * @}
+ *
+ * @defgroup line_settings Line settings objects
+ * @{
+ *
+ * Functions for manipulating line settings objects.
+ *
+ * Line settings object contains a set of line properties that can be used
+ * when requesting lines or reconfiguring an existing request.
+ *
+ * Mutators in general can only fail if the new property value is invalid. The
+ * return values can be safely ignored - the object remains valid even after
+ * a mutator fails and simply uses the sane default appropriate for given
+ * property.
+ */
+
+/**
+ * @brief Create a new line settings object.
+ * @return New line settings object or NULL on error. The returned object must
+ *         be freed by the caller using ::gpiod_line_settings_free.
+ */
+struct gpiod_line_settings *gpiod_line_settings_new(void);
+
+/**
+ * @brief Free the line settings object and release all associated resources.
+ * @param settings Line settings object.
+ */
+void gpiod_line_settings_free(struct gpiod_line_settings *settings);
+
+/**
+ * @brief Reset the line settings object to its default values.
+ * @param settings Line settings object.
+ */
+void gpiod_line_settings_reset(struct gpiod_line_settings *settings);
+
+/**
+ * @brief Copy the line settings object.
+ * @param settings Line settings object to copy.
+ * @return New line settings object that must be freed using
+ *         ::gpiod_line_settings_free or NULL on failure.
+ */
+struct gpiod_line_settings *
+gpiod_line_settings_copy(struct gpiod_line_settings *settings);
+
+/**
+ * @brief Set direction.
+ * @param settings Line settings object.
+ * @param direction New direction.
+ * @return 0 on success, -1 on error.
+ */
+int gpiod_line_settings_set_direction(struct gpiod_line_settings *settings,
+                                     enum gpiod_line_direction direction);
+
+/**
+ * @brief Get direction.
+ * @param settings Line settings object.
+ * @return Current direction.
+ */
+enum gpiod_line_direction
+gpiod_line_settings_get_direction(struct gpiod_line_settings *settings);
+
+/**
+ * @brief Set edge detection.
+ * @param settings Line settings object.
+ * @param edge New edge detection setting.
+ * @return 0 on success, -1 on failure.
+ */
+int gpiod_line_settings_set_edge_detection(struct gpiod_line_settings *settings,
+                                          enum gpiod_line_edge edge);
+
+/**
+ * @brief Get edge detection.
+ * @param settings Line settings object.
+ * @return Current edge detection setting.
+ */
+enum gpiod_line_edge
+gpiod_line_settings_get_edge_detection(struct gpiod_line_settings *settings);
+
+/**
+ * @brief Set bias.
+ * @param settings Line settings object.
+ * @param bias New bias.
+ * @return 0 on success, -1 on failure.
+ */
+int gpiod_line_settings_set_bias(struct gpiod_line_settings *settings,
+                                enum gpiod_line_bias bias);
+
+/**
+ * @brief Get bias.
+ * @param settings Line settings object.
+ * @return Current bias setting.
+ */
+enum gpiod_line_bias
+gpiod_line_settings_get_bias(struct gpiod_line_settings *settings);
+
+/**
+ * @brief Set drive.
+ * @param settings Line settings object.
+ * @param drive New drive setting.
+ * @return 0 on success, -1 on failure.
+ */
+int gpiod_line_settings_set_drive(struct gpiod_line_settings *settings,
+                                 enum gpiod_line_drive drive);
+
+/**
+ * @brief Get drive.
+ * @param settings Line settings object.
+ * @return Current drive setting.
+ */
+enum gpiod_line_drive
+gpiod_line_settings_get_drive(struct gpiod_line_settings *settings);
+
+/**
+ * @brief Set active-low setting.
+ * @param settings Line settings object.
+ * @param active_low New active-low setting.
+ */
+void gpiod_line_settings_set_active_low(struct gpiod_line_settings *settings,
+                                       bool active_low);
+
+/**
+ * @brief Get active-low setting.
+ * @param settings Line settings object.
+ * @return True if active-low is enabled, false otherwise.
+ */
+bool gpiod_line_settings_get_active_low(struct gpiod_line_settings *settings);
+
+/**
+ * @brief Set debounce period.
+ * @param settings Line settings object.
+ * @param period New debounce period in microseconds.
+ */
+void
+gpiod_line_settings_set_debounce_period_us(struct gpiod_line_settings *settings,
+                                          unsigned long period);
+
+/**
+ * @brief Get debounce period.
+ * @param settings Line settings object.
+ * @return Current debounce period in microseconds.
+ */
+unsigned long
+gpiod_line_settings_get_debounce_period_us(
+               struct gpiod_line_settings *settings);
+
+/**
+ * @brief Set event clock.
+ * @param settings Line settings object.
+ * @param event_clock New event clock.
+ * @return 0 on success, -1 on failure.
+ */
+int gpiod_line_settings_set_event_clock(struct gpiod_line_settings *settings,
+                                       enum gpiod_line_clock event_clock);
+
+/**
+ * @brief Get event clock setting.
+ * @param settings Line settings object.
+ * @return Current event clock setting.
+ */
+enum gpiod_line_clock
+gpiod_line_settings_get_event_clock(struct gpiod_line_settings *settings);
+
+/**
+ * @brief Set the output value.
+ * @param settings Line settings object.
+ * @param value New output value.
+ * @return 0 on success, -1 on failure.
+ */
+int gpiod_line_settings_set_output_value(struct gpiod_line_settings *settings,
+                                        enum gpiod_line_value value);
+
+/**
+ * @brief Get the output value.
+ * @param settings Line settings object.
+ * @return Current output value.
+ */
+enum gpiod_line_value
+gpiod_line_settings_get_output_value(struct gpiod_line_settings *settings);
+
+/**
+ * @}
+ *
+ * @defgroup line_config Line configuration objects
+ * @{
+ *
+ * Functions for manipulating line configuration objects.
+ *
+ * The line-config object contains the configuration for lines that can be
+ * used in two cases:
+ *  - when making a line request
+ *  - when reconfiguring a set of already requested lines.
+ *
+ * A new line-config object is empty. Using it in a request will lead to an
+ * error. In order to a line-config to become useful, it needs to be assigned
+ * at least one offset-to-settings mapping by calling
+ * ::gpiod_line_config_add_line_settings.
+ *
+ * When calling ::gpiod_chip_request_lines, the library will request all
+ * offsets that were assigned settings in the order that they were assigned.
+ * If any of the offsets was duplicated, the last one will take precedence.
+ */
+
+/**
+ * @brief Create a new line config object.
+ * @return New line config object or NULL on error. The returned object must
+ *         be freed by the caller using ::gpiod_line_config_free.
+ */
+struct gpiod_line_config *gpiod_line_config_new(void);
+
+/**
+ * @brief Free the line config object and release all associated resources.
+ * @param config Line config object to free.
+ */
+void gpiod_line_config_free(struct gpiod_line_config *config);
+
+/**
+ * @brief Reset the line config object.
+ * @param config Line config object to free.
+ *
+ * Resets the entire configuration stored in the object. This is useful if
+ * the user wants to reuse the object without reallocating it.
+ */
+void gpiod_line_config_reset(struct gpiod_line_config *config);
+
+/**
+ * @brief Add line settings for a set of offsets.
+ * @param config Line config object.
+ * @param offsets Array of offsets for which to apply the settings.
+ * @param num_offsets Number of offsets stored in the offsets array.
+ * @param settings Line settings to apply.
+ * @return 0 on success, -1 on failure.
+ */
+int gpiod_line_config_add_line_settings(struct gpiod_line_config *config,
+                                       const unsigned int *offsets,
+                                       size_t num_offsets,
+                                       struct gpiod_line_settings *settings);
+
+/**
+ * @brief Get line settings for offset.
+ * @param config Line config object.
+ * @param offset Offset for which to get line settings.
+ * @return New line settings object (must be freed by the caller) or NULL on
+ *         error.
+ */
+struct gpiod_line_settings *
+gpiod_line_config_get_line_settings(struct gpiod_line_config *config,
+                                   unsigned int offset);
+
+/**
+ * @brief Set output values for a number of lines.
+ * @param config Line config object.
+ * @param values Buffer containing the output values.
+ * @param num_values Number of values in the buffer.
+ * @return 0 on success, -1 on error.
+ *
+ * This is a helper that allows users to set multiple (potentially different)
+ * output values at once while using the same line settings object. Instead of
+ * modifying the output value in the settings object and calling
+ * ::gpiod_line_config_add_line_settings multiple times, we can specify the
+ * settings, add them for a set of offsets and then call this function to
+ * set the output values.
+ *
+ * Values set by this function override whatever values were specified in the
+ * regular line settings.
+ *
+ * Each value must be associated with the line identified by the corresponding
+ * entry in the offset array filled by
+ * ::gpiod_line_request_get_requested_offsets.
+ */
+int gpiod_line_config_set_output_values(struct gpiod_line_config *config,
+                                       const enum gpiod_line_value *values,
+                                       size_t num_values);
+
+/**
+ * @brief Get the number of configured line offsets.
+ * @param config Line config object.
+ * @return Number of offsets for which line settings have been added.
+ */
+size_t
+gpiod_line_config_get_num_configured_offsets(struct gpiod_line_config *config);
+
+/**
+ * @brief Get configured offsets.
+ * @param config Line config object.
+ * @param offsets Array to store offsets.
+ * @param max_offsets Number of offsets that can be stored in the offsets array.
+ * @return Number of offsets stored in the offsets array.
+ *
+ * If max_offsets is lower than the number of lines actually requested (this
+ * value can be retrieved using ::gpiod_line_config_get_num_configured_offsets),
+ * then only up to max_lines offsets will be stored in offsets.
+ */
+size_t
+gpiod_line_config_get_configured_offsets(struct gpiod_line_config *config,
+                                        unsigned int *offsets,
+                                        size_t max_offsets);
+
+/**
+ * @}
+ *
+ * @defgroup request_config Request configuration objects
+ * @{
+ *
+ * Functions for manipulating request configuration objects.
+ *
+ * Request config objects are used to pass a set of options to the kernel at
+ * the time of the line request. The mutators don't return error values. If the
+ * values are invalid, in general they are silently adjusted to acceptable
+ * ranges.
+ */
+
+/**
+ * @brief Create a new request config object.
+ * @return New request config object or NULL on error. The returned object must
+ *         be freed by the caller using ::gpiod_request_config_free.
+ */
+struct gpiod_request_config *gpiod_request_config_new(void);
+
+/**
+ * @brief Free the request config object and release all associated resources.
+ * @param config Line config object.
+ */
+void gpiod_request_config_free(struct gpiod_request_config *config);
+
+/**
+ * @brief Set the consumer name for the request.
+ * @param config Request config object.
+ * @param consumer Consumer name.
+ * @note If the consumer string is too long, it will be truncated to the max
+ *       accepted length.
+ */
+void gpiod_request_config_set_consumer(struct gpiod_request_config *config,
+                                      const char *consumer);
+
+/**
+ * @brief Get the consumer name configured in the request config.
+ * @param config Request config object.
+ * @return Consumer name stored in the request config.
+ */
+const char *
+gpiod_request_config_get_consumer(struct gpiod_request_config *config);
+
+/**
+ * @brief Set the size of the kernel event buffer for the request.
+ * @param config Request config object.
+ * @param event_buffer_size New event buffer size.
+ * @note The kernel may adjust the value if it's too high. If set to 0, the
+ *       default value will be used.
+ * @note The kernel buffer is distinct from and independent of the user space
+ *       buffer (::gpiod_edge_event_buffer_new).
+ */
+void
+gpiod_request_config_set_event_buffer_size(struct gpiod_request_config *config,
+                                          size_t event_buffer_size);
+
+/**
+ * @brief Get the edge event buffer size for the request config.
+ * @param config Request config object.
+ * @return Edge event buffer size setting from the request config.
+ */
+size_t
+gpiod_request_config_get_event_buffer_size(struct gpiod_request_config *config);
+
+/**
+ * @}
+ *
+ * @defgroup line_request Line request operations
+ * @{
+ *
+ * Functions allowing interactions with requested lines.
+ */
+
+/**
+ * @brief Release the requested lines and free all associated resources.
+ * @param request Line request object to release.
+ */
+void gpiod_line_request_release(struct gpiod_line_request *request);
+
+/**
+ * @brief Get the name of the chip this request was made on.
+ * @param request Line request object.
+ * @return Name the GPIO chip device. The returned pointer is valid for the
+ * lifetime of the request object and must not be freed by the caller.
+ */
+const char *
+gpiod_line_request_get_chip_name(struct gpiod_line_request *request);
+
+/**
+ * @brief Get the number of lines in the request.
+ * @param request Line request object.
+ * @return Number of requested lines.
+ */
+size_t
+gpiod_line_request_get_num_requested_lines(struct gpiod_line_request *request);
+
+/**
+ * @brief Get the offsets of the lines in the request.
+ * @param request Line request object.
+ * @param offsets Array to store offsets.
+ * @param max_offsets Number of offsets that can be stored in the offsets array.
+ * @return Number of offsets stored in the offsets array.
+ *
+ * If max_offsets is lower than the number of lines actually requested (this
+ * value can be retrieved using ::gpiod_line_request_get_num_requested_lines),
+ * then only up to max_lines offsets will be stored in offsets.
+ */
+size_t
+gpiod_line_request_get_requested_offsets(struct gpiod_line_request *request,
+                                        unsigned int *offsets,
+                                        size_t max_offsets);
+
+/**
+ * @brief Get the value of a single requested line.
+ * @param request Line request object.
+ * @param offset The offset of the line of which the value should be read.
+ * @return Returns 1 or 0 on success and -1 on error.
+ */
+enum gpiod_line_value
+gpiod_line_request_get_value(struct gpiod_line_request *request,
+                            unsigned int offset);
+
+/**
+ * @brief Get the values of a subset of requested lines.
+ * @param request GPIO line request.
+ * @param num_values Number of lines for which to read values.
+ * @param offsets Array of offsets identifying the subset of requested lines
+ *                from which to read values.
+ * @param values Array in which the values will be stored. Must be sized
+ *               to hold \p num_values entries. Each value is associated with
+ *               the line identified by the corresponding entry in \p offsets.
+ * @return 0 on success, -1 on failure.
+ */
+int gpiod_line_request_get_values_subset(struct gpiod_line_request *request,
+                                        size_t num_values,
+                                        const unsigned int *offsets,
+                                        enum gpiod_line_value *values);
+
+/**
+ * @brief Get the values of all requested lines.
+ * @param request GPIO line request.
+ * @param values Array in which the values will be stored. Must be sized to
+ *               hold the number of lines filled by
+ *               ::gpiod_line_request_get_num_requested_lines.
+ *               Each value is associated with the line identified by the
+ *               corresponding entry in the offset array filled by
+ *               ::gpiod_line_request_get_requested_offsets.
+ * @return 0 on success, -1 on failure.
+ */
+int gpiod_line_request_get_values(struct gpiod_line_request *request,
+                                 enum gpiod_line_value *values);
+
+/**
+ * @brief Set the value of a single requested line.
+ * @param request Line request object.
+ * @param offset The offset of the line for which the value should be set.
+ * @param value Value to set.
+ * @return 0 on success, -1 on failure.
+ */
+int gpiod_line_request_set_value(struct gpiod_line_request *request,
+                                unsigned int offset,
+                                enum gpiod_line_value value);
+
+/**
+ * @brief Set the values of a subset of requested lines.
+ * @param request GPIO line request.
+ * @param num_values Number of lines for which to set values.
+ * @param offsets Array of offsets, containing the number of entries specified
+ *                by \p num_values, identifying the requested lines for
+ *                which to set values.
+ * @param values Array of values to set, containing the number of entries
+ *               specified by \p num_values. Each value is associated with the
+ *               line identified by the corresponding entry in \p offsets.
+ * @return 0 on success, -1 on failure.
+ */
+int gpiod_line_request_set_values_subset(struct gpiod_line_request *request,
+                                        size_t num_values,
+                                        const unsigned int *offsets,
+                                        const enum gpiod_line_value *values);
+
+/**
+ * @brief Set the values of all lines associated with a request.
+ * @param request GPIO line request.
+ * @param values Array containing the values to set. Must be sized to
+ *               contain the number of lines filled by
+ *               ::gpiod_line_request_get_num_requested_lines.
+ *               Each value is associated with the line identified by the
+ *               corresponding entry in the offset array filled by
+ *               ::gpiod_line_request_get_requested_offsets.
+ * @return 0 on success, -1 on failure.
+ */
+int gpiod_line_request_set_values(struct gpiod_line_request *request,
+                                 const enum gpiod_line_value *values);
+
+/**
+ * @brief Update the configuration of lines associated with a line request.
+ * @param request GPIO line request.
+ * @param config New line config to apply.
+ * @return 0 on success, -1 on failure.
+ * @note The new line configuration completely replaces the old.
+ * @note Any requested lines without overrides are configured to the requested
+ *       defaults.
+ * @note Any configured overrides for lines that have not been requested
+ *       are silently ignored.
+ */
+int gpiod_line_request_reconfigure_lines(struct gpiod_line_request *request,
+                                        struct gpiod_line_config *config);
+
+/**
+ * @brief Get the file descriptor associated with a line request.
+ * @param request GPIO line request.
+ * @return The file descriptor associated with the request.
+ *         This function never fails.
+ *         The returned file descriptor must not be closed by the caller.
+ *         Call ::gpiod_line_request_release to close the file.
+ */
+int gpiod_line_request_get_fd(struct gpiod_line_request *request);
+
+/**
+ * @brief Wait for edge events on any of the requested lines.
+ * @param request GPIO line request.
+ * @param timeout_ns Wait time limit in nanoseconds. If set to 0, the function
+ *                   returns immediatelly. If set to a negative number, the
+ *                   function blocks indefinitely until an event becomes
+ *                   available.
+ * @return 0 if wait timed out, -1 if an error occurred, 1 if an event is
+ *         pending.
+ *
+ * Lines must have edge detection set for edge events to be emitted.
+ * By default edge detection is disabled.
+ */
+int gpiod_line_request_wait_edge_events(struct gpiod_line_request *request,
+                                       int64_t timeout_ns);
+
+/**
+ * @brief Read a number of edge events from a line request.
+ * @param request GPIO line request.
+ * @param buffer Edge event buffer, sized to hold at least \p max_events.
+ * @param max_events Maximum number of events to read.
+ * @return On success returns the number of events read from the file
+ *         descriptor, on failure return -1.
+ * @note This function will block if no event was queued for the line request.
+ * @note Any exising events in the buffer are overwritten. This is not an
+ *       append operation.
+ */
+int gpiod_line_request_read_edge_events(struct gpiod_line_request *request,
+                                       struct gpiod_edge_event_buffer *buffer,
+                                       size_t max_events);
+
+/**
+ * @}
+ *
+ * @defgroup edge_event Line edge events handling
+ * @{
+ *
+ * Functions and data types for handling edge events.
+ *
+ * An edge event object contains information about a single line edge event.
+ * It contains the event type, timestamp and the offset of the line on which
+ * the event occurred as well as two sequence numbers (global for all lines
+ * in the associated request and local for this line only).
+ *
+ * Edge events are stored into an edge-event buffer object to improve
+ * performance and to limit the number of memory allocations when a large
+ * number of events are being read.
+ */
+
+/**
+ * @brief Event types.
+ */
+enum gpiod_edge_event_type {
+       GPIOD_EDGE_EVENT_RISING_EDGE = 1,
+       /**< Rising edge event. */
+       GPIOD_EDGE_EVENT_FALLING_EDGE,
+       /**< Falling edge event. */
+};
+
+/**
+ * @brief Free the edge event object.
+ * @param event Edge event object to free.
+ */
+void gpiod_edge_event_free(struct gpiod_edge_event *event);
+
+/**
+ * @brief Copy the edge event object.
+ * @param event Edge event to copy.
+ * @return Copy of the edge event or NULL on error. The returned object must
+ *         be freed by the caller using ::gpiod_edge_event_free.
+ */
+struct gpiod_edge_event *gpiod_edge_event_copy(struct gpiod_edge_event *event);
+
+/**
+ * @brief Get the event type.
+ * @param event GPIO edge event.
+ * @return The event type (::GPIOD_EDGE_EVENT_RISING_EDGE or
+ *         ::GPIOD_EDGE_EVENT_FALLING_EDGE).
+ */
+enum gpiod_edge_event_type
+gpiod_edge_event_get_event_type(struct gpiod_edge_event *event);
+
+/**
+ * @brief Get the timestamp of the event.
+ * @param event GPIO edge event.
+ * @return Timestamp in nanoseconds.
+ * @note The source clock for the timestamp depends on the event_clock
+ *       setting for the line.
+ */
+uint64_t gpiod_edge_event_get_timestamp_ns(struct gpiod_edge_event *event);
+
+/**
+ * @brief Get the offset of the line which triggered the event.
+ * @param event GPIO edge event.
+ * @return Line offset.
+ */
+unsigned int gpiod_edge_event_get_line_offset(struct gpiod_edge_event *event);
+
+/**
+ * @brief Get the global sequence number of the event.
+ * @param event GPIO edge event.
+ * @return Sequence number of the event in the series of events for all lines
+ *         in the associated line request.
+ */
+unsigned long gpiod_edge_event_get_global_seqno(struct gpiod_edge_event *event);
+
+/**
+ * @brief Get the event sequence number specific to the line.
+ * @param event GPIO edge event.
+ * @return Sequence number of the event in the series of events only for this
+ *         line within the lifetime of the associated line request.
+ */
+unsigned long gpiod_edge_event_get_line_seqno(struct gpiod_edge_event *event);
+
+/**
+ * @brief Create a new edge event buffer.
+ * @param capacity Number of events the buffer can store (min = 1, max = 1024).
+ * @return New edge event buffer or NULL on error.
+ * @note If capacity equals 0, it will be set to a default value of 64. If
+ *       capacity is larger than 1024, it will be limited to 1024.
+ * @note The user space buffer is independent of the kernel buffer
+ *       (::gpiod_request_config_set_event_buffer_size). As the user space
+ *       buffer is filled from the kernel buffer, there is no benefit making
+ *       the user space buffer larger than the kernel buffer.
+ *       The default kernel buffer size for each request is (16 * num_lines).
+ */
+struct gpiod_edge_event_buffer *
+gpiod_edge_event_buffer_new(size_t capacity);
+
+/**
+ * @brief Get the capacity (the max number of events that can be stored) of
+ *        the event buffer.
+ * @param buffer Edge event buffer.
+ * @return The capacity of the buffer.
+ */
+size_t
+gpiod_edge_event_buffer_get_capacity(struct gpiod_edge_event_buffer *buffer);
+
+/**
+ * @brief Free the edge event buffer and release all associated resources.
+ * @param buffer Edge event buffer to free.
+ */
+void gpiod_edge_event_buffer_free(struct gpiod_edge_event_buffer *buffer);
+
+/**
+ * @brief Get an event stored in the buffer.
+ * @param buffer Edge event buffer.
+ * @param index Index of the event in the buffer.
+ * @return Pointer to an event stored in the buffer. The lifetime of the
+ *         event is tied to the buffer object. Users must not free the event
+ *         returned by this function.
+ * @warning Thread-safety:
+ *          Since events are tied to the buffer instance, different threads
+ *          may not operate on the buffer and any associated events at the same
+ *          time. Events can be copied using ::gpiod_edge_event_copy in order
+ *          to create a standalone objects - which each may safely be used from
+ *          a different thread concurrently.
+ */
+struct gpiod_edge_event *
+gpiod_edge_event_buffer_get_event(struct gpiod_edge_event_buffer *buffer,
+                                 unsigned long index);
+
+/**
+ * @brief Get the number of events a buffer has stored.
+ * @param buffer Edge event buffer.
+ * @return Number of events stored in the buffer.
+ */
+size_t
+gpiod_edge_event_buffer_get_num_events(struct gpiod_edge_event_buffer *buffer);
+
+/**
+ * @}
+ *
+ * @defgroup misc Stuff that didn't fit anywhere else
+ * @{
+ *
+ * Various libgpiod-related functions.
+ */
+
+/**
+ * @brief Check if the file pointed to by path is a GPIO chip character device.
+ * @param path Path to check.
+ * @return True if the file exists and is either a GPIO chip character device
+ *         or a symbolic link to one.
+ */
+bool gpiod_is_gpiochip_device(const char *path);
+
+/**
+ * @brief Get the API version of the library as a human-readable string.
+ * @return A valid pointer to a human-readable string containing the library
+ *         version. The pointer is valid for the lifetime of the program and
+ *         must not be freed by the caller.
+ */
+const char *gpiod_api_version(void);
+
+/**
+ * @}
+ */
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* __LIBGPIOD_GPIOD_H__ */
diff --git a/lib/Makefile.am b/lib/Makefile.am
new file mode 100644 (file)
index 0000000..3e7114b
--- /dev/null
@@ -0,0 +1,28 @@
+# SPDX-License-Identifier: GPL-2.0-or-later
+# SPDX-FileCopyrightText: 2017-2021 Bartosz Golaszewski <bartekgola@gmail.com>
+
+lib_LTLIBRARIES = libgpiod.la
+libgpiod_la_SOURCES = \
+       chip.c \
+       chip-info.c \
+       edge-event.c \
+       info-event.c \
+       internal.h \
+       internal.c \
+       line-config.c \
+       line-info.c \
+       line-request.c \
+       line-settings.c \
+       misc.c \
+       request-config.c \
+       uapi/gpio.h
+
+libgpiod_la_CFLAGS = -Wall -Wextra -g -std=gnu89
+libgpiod_la_CFLAGS += -fvisibility=hidden -I$(top_srcdir)/include/
+libgpiod_la_CFLAGS += -include $(top_builddir)/config.h
+libgpiod_la_CFLAGS += $(PROFILING_CFLAGS)
+libgpiod_la_LDFLAGS = -version-info $(subst .,:,$(ABI_VERSION))
+libgpiod_la_LDFLAGS+= $(PROFILING_LDFLAGS)
+
+pkgconfigdir = $(libdir)/pkgconfig
+pkgconfig_DATA = libgpiod.pc
diff --git a/lib/chip-info.c b/lib/chip-info.c
new file mode 100644 (file)
index 0000000..2d9f44d
--- /dev/null
@@ -0,0 +1,73 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// SPDX-FileCopyrightText: 2022 Bartosz Golaszewski <brgl@bgdev.pl>
+
+#include <assert.h>
+#include <gpiod.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "internal.h"
+
+struct gpiod_chip_info {
+       size_t num_lines;
+       char name[GPIO_MAX_NAME_SIZE + 1];
+       char label[GPIO_MAX_NAME_SIZE + 1];
+};
+
+GPIOD_API void gpiod_chip_info_free(struct gpiod_chip_info *info)
+{
+       free(info);
+}
+
+GPIOD_API const char *gpiod_chip_info_get_name(struct gpiod_chip_info *info)
+{
+       assert(info);
+
+       return info->name;
+}
+
+GPIOD_API const char *gpiod_chip_info_get_label(struct gpiod_chip_info *info)
+{
+       assert(info);
+
+       return info->label;
+}
+
+GPIOD_API size_t gpiod_chip_info_get_num_lines(struct gpiod_chip_info *info)
+{
+       assert(info);
+
+       return info->num_lines;
+}
+
+struct gpiod_chip_info *
+gpiod_chip_info_from_uapi(struct gpiochip_info *uapi_info)
+{
+       struct gpiod_chip_info *info;
+
+       info = malloc(sizeof(*info));
+       if (!info)
+               return NULL;
+
+       memset(info, 0, sizeof(*info));
+
+       info->num_lines = uapi_info->lines;
+
+       /*
+        * GPIO device must have a name - don't bother checking this field. In
+        * the worst case (would have to be a weird kernel bug) it'll be empty.
+        */
+       strncpy(info->name, uapi_info->name, sizeof(info->name));
+
+       /*
+        * The kernel sets the label of a GPIO device to "unknown" if it
+        * hasn't been defined in DT, board file etc. On the off-chance that
+        * we got an empty string, do the same.
+        */
+       if (uapi_info->label[0] == '\0')
+               strncpy(info->label, "unknown", sizeof(info->label));
+       else
+               strncpy(info->label, uapi_info->label, sizeof(info->label));
+
+       return info;
+}
diff --git a/lib/chip.c b/lib/chip.c
new file mode 100644 (file)
index 0000000..611eb32
--- /dev/null
@@ -0,0 +1,251 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// SPDX-FileCopyrightText: 2021 Bartosz Golaszewski <brgl@bgdev.pl>
+
+#include <assert.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <gpiod.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+#include "internal.h"
+
+struct gpiod_chip {
+       int fd;
+       char *path;
+};
+
+GPIOD_API struct gpiod_chip *gpiod_chip_open(const char *path)
+{
+       struct gpiod_chip *chip;
+       int fd;
+
+       if (!path) {
+               errno = EINVAL;
+               return NULL;
+       }
+
+       if (!gpiod_check_gpiochip_device(path, true))
+               return NULL;
+
+       fd = open(path, O_RDWR | O_CLOEXEC);
+       if (fd < 0)
+               return NULL;
+
+       chip = malloc(sizeof(*chip));
+       if (!chip)
+               goto err_close_fd;
+
+       memset(chip, 0, sizeof(*chip));
+
+       chip->path = strdup(path);
+       if (!chip->path)
+               goto err_free_chip;
+
+       chip->fd = fd;
+
+       return chip;
+
+err_free_chip:
+       free(chip);
+err_close_fd:
+       close(fd);
+
+       return NULL;
+}
+
+GPIOD_API void gpiod_chip_close(struct gpiod_chip *chip)
+{
+       if (!chip)
+               return;
+
+       close(chip->fd);
+       free(chip->path);
+       free(chip);
+}
+
+static int read_chip_info(int fd, struct gpiochip_info *info)
+{
+       int ret;
+
+       memset(info, 0, sizeof(*info));
+
+       ret = gpiod_ioctl(fd, GPIO_GET_CHIPINFO_IOCTL, info);
+       if (ret)
+               return -1;
+
+       return 0;
+}
+
+GPIOD_API struct gpiod_chip_info *gpiod_chip_get_info(struct gpiod_chip *chip)
+{
+       struct gpiochip_info info;
+       int ret;
+
+       assert(chip);
+
+       ret = read_chip_info(chip->fd, &info);
+       if (ret)
+               return NULL;
+
+       return gpiod_chip_info_from_uapi(&info);
+}
+
+GPIOD_API const char *gpiod_chip_get_path(struct gpiod_chip *chip)
+{
+       assert(chip);
+
+       return chip->path;
+}
+
+static int chip_read_line_info(int fd, unsigned int offset,
+                              struct gpio_v2_line_info *info, bool watch)
+{
+       int ret, cmd;
+
+       memset(info, 0, sizeof(*info));
+       info->offset = offset;
+
+       cmd = watch ? GPIO_V2_GET_LINEINFO_WATCH_IOCTL :
+                     GPIO_V2_GET_LINEINFO_IOCTL;
+
+       ret = gpiod_ioctl(fd, cmd, info);
+       if (ret)
+               return -1;
+
+       return 0;
+}
+
+static struct gpiod_line_info *
+chip_get_line_info(struct gpiod_chip *chip, unsigned int offset, bool watch)
+{
+       struct gpio_v2_line_info info;
+       int ret;
+
+       assert(chip);
+
+       ret = chip_read_line_info(chip->fd, offset, &info, watch);
+       if (ret)
+               return NULL;
+
+       return gpiod_line_info_from_uapi(&info);
+}
+
+GPIOD_API struct gpiod_line_info *
+gpiod_chip_get_line_info(struct gpiod_chip *chip, unsigned int offset)
+{
+       return chip_get_line_info(chip, offset, false);
+}
+
+GPIOD_API struct gpiod_line_info *
+gpiod_chip_watch_line_info(struct gpiod_chip *chip, unsigned int offset)
+{
+       return chip_get_line_info(chip, offset, true);
+}
+
+GPIOD_API int gpiod_chip_unwatch_line_info(struct gpiod_chip *chip,
+                                          unsigned int offset)
+{
+       assert(chip);
+
+       return gpiod_ioctl(chip->fd, GPIO_GET_LINEINFO_UNWATCH_IOCTL, &offset);
+}
+
+GPIOD_API int gpiod_chip_get_fd(struct gpiod_chip *chip)
+{
+       assert(chip);
+
+       return chip->fd;
+}
+
+GPIOD_API int gpiod_chip_wait_info_event(struct gpiod_chip *chip,
+                                        int64_t timeout_ns)
+{
+       assert(chip);
+
+       return gpiod_poll_fd(chip->fd, timeout_ns);
+}
+
+GPIOD_API struct gpiod_info_event *
+gpiod_chip_read_info_event(struct gpiod_chip *chip)
+{
+       assert(chip);
+
+       return gpiod_info_event_read_fd(chip->fd);
+}
+
+GPIOD_API int gpiod_chip_get_line_offset_from_name(struct gpiod_chip *chip,
+                                                  const char *name)
+{
+       struct gpio_v2_line_info linfo;
+       struct gpiochip_info chinfo;
+       unsigned int offset;
+       int ret;
+
+       assert(chip);
+
+       if (!name) {
+               errno = EINVAL;
+               return -1;
+       }
+
+       ret = read_chip_info(chip->fd, &chinfo);
+       if (ret)
+               return -1;
+
+       for (offset = 0; offset < chinfo.lines; offset++) {
+               ret = chip_read_line_info(chip->fd, offset, &linfo, false);
+               if (ret)
+                       return -1;
+
+               if (strcmp(name, linfo.name) == 0)
+                       return offset;
+       }
+
+       errno = ENOENT;
+       return -1;
+}
+
+GPIOD_API struct gpiod_line_request *
+gpiod_chip_request_lines(struct gpiod_chip *chip,
+                        struct gpiod_request_config *req_cfg,
+                        struct gpiod_line_config *line_cfg)
+{
+       struct gpio_v2_line_request uapi_req;
+       struct gpiod_line_request *request;
+       struct gpiochip_info info;
+       int ret;
+
+       assert(chip);
+
+       if (!line_cfg) {
+               errno = EINVAL;
+               return NULL;
+       }
+
+       memset(&uapi_req, 0, sizeof(uapi_req));
+
+       if (req_cfg)
+               gpiod_request_config_to_uapi(req_cfg, &uapi_req);
+
+       ret = gpiod_line_config_to_uapi(line_cfg, &uapi_req);
+       if (ret)
+               return NULL;
+
+       ret = read_chip_info(chip->fd, &info);
+       if (ret)
+               return NULL;
+
+       ret = gpiod_ioctl(chip->fd, GPIO_V2_GET_LINE_IOCTL, &uapi_req);
+       if (ret)
+               return NULL;
+
+       request = gpiod_line_request_from_uapi(&uapi_req, info.name);
+       if (!request) {
+               close(uapi_req.fd);
+               return NULL;
+       }
+
+       return request;
+}
diff --git a/lib/edge-event.c b/lib/edge-event.c
new file mode 100644 (file)
index 0000000..8ec355f
--- /dev/null
@@ -0,0 +1,212 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// SPDX-FileCopyrightText: 2021 Bartosz Golaszewski <brgl@bgdev.pl>
+
+#include <assert.h>
+#include <errno.h>
+#include <gpiod.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+#include "internal.h"
+
+/* As defined in the kernel. */
+#define EVENT_BUFFER_MAX_CAPACITY (GPIO_V2_LINES_MAX * 16)
+
+struct gpiod_edge_event {
+       enum gpiod_edge_event_type event_type;
+       uint64_t timestamp;
+       unsigned int line_offset;
+       unsigned long global_seqno;
+       unsigned long line_seqno;
+};
+
+struct gpiod_edge_event_buffer {
+       size_t capacity;
+       size_t num_events;
+       struct gpiod_edge_event *events;
+       struct gpio_v2_line_event *event_data;
+};
+
+GPIOD_API void gpiod_edge_event_free(struct gpiod_edge_event *event)
+{
+       free(event);
+}
+
+GPIOD_API struct gpiod_edge_event *
+gpiod_edge_event_copy(struct gpiod_edge_event *event)
+{
+       struct gpiod_edge_event *copy;
+
+       assert(event);
+
+       copy = malloc(sizeof(*event));
+       if (!copy)
+               return NULL;
+
+       memcpy(copy, event, sizeof(*event));
+
+       return copy;
+}
+
+GPIOD_API enum gpiod_edge_event_type
+gpiod_edge_event_get_event_type(struct gpiod_edge_event *event)
+{
+       assert(event);
+
+       return event->event_type;
+}
+
+GPIOD_API uint64_t
+gpiod_edge_event_get_timestamp_ns(struct gpiod_edge_event *event)
+{
+       assert(event);
+
+       return event->timestamp;
+}
+
+GPIOD_API unsigned int
+gpiod_edge_event_get_line_offset(struct gpiod_edge_event *event)
+{
+       assert(event);
+
+       return event->line_offset;
+}
+
+GPIOD_API unsigned long
+gpiod_edge_event_get_global_seqno(struct gpiod_edge_event *event)
+{
+       assert(event);
+
+       return event->global_seqno;
+}
+
+GPIOD_API unsigned long
+gpiod_edge_event_get_line_seqno(struct gpiod_edge_event *event)
+{
+       assert(event);
+
+       return event->line_seqno;
+}
+
+GPIOD_API struct gpiod_edge_event_buffer *
+gpiod_edge_event_buffer_new(size_t capacity)
+{
+       struct gpiod_edge_event_buffer *buf;
+
+       if (capacity == 0)
+               capacity = 64;
+       if (capacity > EVENT_BUFFER_MAX_CAPACITY)
+               capacity = EVENT_BUFFER_MAX_CAPACITY;
+
+       buf = malloc(sizeof(*buf));
+       if (!buf)
+               return NULL;
+
+       memset(buf, 0, sizeof(*buf));
+       buf->capacity = capacity;
+
+       buf->events = calloc(capacity, sizeof(*buf->events));
+       if (!buf->events) {
+               free(buf);
+               return NULL;
+       }
+
+       buf->event_data = calloc(capacity, sizeof(*buf->event_data));
+       if (!buf->event_data) {
+               free(buf->events);
+               free(buf);
+               return NULL;
+       }
+
+       return buf;
+}
+
+GPIOD_API size_t
+gpiod_edge_event_buffer_get_capacity(struct gpiod_edge_event_buffer *buffer)
+{
+       assert(buffer);
+
+       return buffer->capacity;
+}
+
+GPIOD_API void
+gpiod_edge_event_buffer_free(struct gpiod_edge_event_buffer *buffer)
+{
+       if (!buffer)
+               return;
+
+       free(buffer->events);
+       free(buffer->event_data);
+       free(buffer);
+}
+
+GPIOD_API struct gpiod_edge_event *
+gpiod_edge_event_buffer_get_event(struct gpiod_edge_event_buffer *buffer,
+                                 unsigned long index)
+{
+       assert(buffer);
+
+       if (index >= buffer->num_events) {
+               errno = EINVAL;
+               return NULL;
+       }
+
+       return &buffer->events[index];
+}
+
+GPIOD_API size_t
+gpiod_edge_event_buffer_get_num_events(struct gpiod_edge_event_buffer *buffer)
+{
+       assert(buffer);
+
+       return buffer->num_events;
+}
+
+int gpiod_edge_event_buffer_read_fd(int fd,
+                                   struct gpiod_edge_event_buffer *buffer,
+                                   size_t max_events)
+{
+       struct gpio_v2_line_event *curr;
+       struct gpiod_edge_event *event;
+       size_t i;
+       ssize_t rd;
+
+       if (!buffer) {
+               errno = EINVAL;
+               return -1;
+       }
+
+       memset(buffer->event_data, 0,
+              sizeof(*buffer->event_data) * buffer->capacity);
+       memset(buffer->events, 0, sizeof(*buffer->events) * buffer->capacity);
+
+       if (max_events > buffer->capacity)
+               max_events = buffer->capacity;
+
+       rd = read(fd, buffer->event_data,
+                 max_events * sizeof(*buffer->event_data));
+       if (rd < 0) {
+               return -1;
+       } else if ((unsigned int)rd < sizeof(*buffer->event_data)) {
+               errno = EIO;
+               return -1;
+       }
+
+       buffer->num_events = rd / sizeof(*buffer->event_data);
+
+       for (i = 0; i < buffer->num_events; i++) {
+               curr = &buffer->event_data[i];
+               event = &buffer->events[i];
+
+               event->line_offset = curr->offset;
+               event->event_type = curr->id == GPIO_V2_LINE_EVENT_RISING_EDGE ?
+                                           GPIOD_EDGE_EVENT_RISING_EDGE :
+                                           GPIOD_EDGE_EVENT_FALLING_EDGE;
+               event->timestamp = curr->timestamp_ns;
+               event->global_seqno = curr->seqno;
+               event->line_seqno = curr->line_seqno;
+       }
+
+       return i;
+}
diff --git a/lib/info-event.c b/lib/info-event.c
new file mode 100644 (file)
index 0000000..0e3ef9b
--- /dev/null
@@ -0,0 +1,105 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// SPDX-FileCopyrightText: 2021 Bartosz Golaszewski <brgl@bgdev.pl>
+
+#include <assert.h>
+#include <errno.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+#include "internal.h"
+
+struct gpiod_info_event {
+       enum gpiod_info_event_type event_type;
+       uint64_t timestamp;
+       struct gpiod_line_info *info;
+};
+
+struct gpiod_info_event *
+gpiod_info_event_from_uapi(struct gpio_v2_line_info_changed *uapi_evt)
+{
+       struct gpiod_info_event *event;
+
+       event = malloc(sizeof(*event));
+       if (!event)
+               return NULL;
+
+       memset(event, 0, sizeof(*event));
+       event->timestamp = uapi_evt->timestamp_ns;
+
+       switch (uapi_evt->event_type) {
+       case GPIOLINE_CHANGED_REQUESTED:
+               event->event_type = GPIOD_INFO_EVENT_LINE_REQUESTED;
+               break;
+       case GPIOLINE_CHANGED_RELEASED:
+               event->event_type = GPIOD_INFO_EVENT_LINE_RELEASED;
+               break;
+       case GPIOLINE_CHANGED_CONFIG:
+               event->event_type = GPIOD_INFO_EVENT_LINE_CONFIG_CHANGED;
+               break;
+       default:
+               /* Can't happen unless there's a bug in the kernel. */
+               errno = ENOMSG;
+               free(event);
+               return NULL;
+       }
+
+       event->info = gpiod_line_info_from_uapi(&uapi_evt->info);
+       if (!event->info) {
+               free(event);
+               return NULL;
+       }
+
+       return event;
+}
+
+GPIOD_API void gpiod_info_event_free(struct gpiod_info_event *event)
+{
+       if (!event)
+               return;
+
+       gpiod_line_info_free(event->info);
+       free(event);
+}
+
+GPIOD_API enum gpiod_info_event_type
+gpiod_info_event_get_event_type(struct gpiod_info_event *event)
+{
+       assert(event);
+
+       return event->event_type;
+}
+
+GPIOD_API uint64_t
+gpiod_info_event_get_timestamp_ns(struct gpiod_info_event *event)
+{
+       assert(event);
+
+       return event->timestamp;
+}
+
+GPIOD_API struct gpiod_line_info *
+gpiod_info_event_get_line_info(struct gpiod_info_event *event)
+{
+       assert(event);
+
+       return event->info;
+}
+
+struct gpiod_info_event *gpiod_info_event_read_fd(int fd)
+{
+       struct gpio_v2_line_info_changed uapi_evt;
+       ssize_t rd;
+
+       memset(&uapi_evt, 0, sizeof(uapi_evt));
+
+       rd = read(fd, &uapi_evt, sizeof(uapi_evt));
+       if (rd < 0) {
+               return NULL;
+       } else if ((unsigned int)rd < sizeof(uapi_evt)) {
+               errno = EIO;
+               return NULL;
+       }
+
+       return gpiod_info_event_from_uapi(&uapi_evt);
+}
diff --git a/lib/internal.c b/lib/internal.c
new file mode 100644 (file)
index 0000000..56cb8b9
--- /dev/null
@@ -0,0 +1,158 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// SPDX-FileCopyrightText: 2021-2022 Bartosz Golaszewski <brgl@bgdev.pl>
+
+#include <errno.h>
+#include <poll.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/ioctl.h>
+#include <sys/stat.h>
+#include <sys/sysmacros.h>
+#include <sys/types.h>
+#include <unistd.h>
+
+#include "internal.h"
+
+bool gpiod_check_gpiochip_device(const char *path, bool set_errno)
+{
+       char *realname, *sysfsp, devpath[64];
+       struct stat statbuf;
+       bool ret = false;
+       int rv;
+
+       if (!path) {
+               errno = EINVAL;
+               goto out;
+       }
+
+       rv = lstat(path, &statbuf);
+       if (rv)
+               goto out;
+
+       /*
+        * Is it a symbolic link? We have to resolve it before checking
+        * the rest.
+        */
+       realname = S_ISLNK(statbuf.st_mode) ? realpath(path, NULL) :
+                                             strdup(path);
+       if (realname == NULL)
+               goto out;
+
+       rv = stat(realname, &statbuf);
+       if (rv)
+               goto out_free_realname;
+
+       /* Is it a character device? */
+       if (!S_ISCHR(statbuf.st_mode)) {
+               errno = ENOTTY;
+               goto out_free_realname;
+       }
+
+       /* Is the device associated with the GPIO subsystem? */
+       snprintf(devpath, sizeof(devpath), "/sys/dev/char/%u:%u/subsystem",
+                major(statbuf.st_rdev), minor(statbuf.st_rdev));
+
+       sysfsp = realpath(devpath, NULL);
+       if (!sysfsp)
+               goto out_free_realname;
+
+       /*
+        * In glibc, if any of the underlying readlink() calls fail (which is
+        * perfectly normal when resolving paths), errno is not cleared.
+        */
+       errno = 0;
+
+       if (strcmp(sysfsp, "/sys/bus/gpio") != 0) {
+               /* This is a character device but not the one we're after. */
+               errno = ENODEV;
+               goto out_free_sysfsp;
+       }
+
+       ret = true;
+
+out_free_sysfsp:
+       free(sysfsp);
+out_free_realname:
+       free(realname);
+out:
+       if (!set_errno)
+               errno = 0;
+       return ret;
+}
+
+int gpiod_poll_fd(int fd, int64_t timeout_ns)
+{
+       struct timespec ts;
+       struct pollfd pfd;
+       int ret;
+
+       memset(&pfd, 0, sizeof(pfd));
+       pfd.fd = fd;
+       pfd.events = POLLIN | POLLPRI;
+
+       if (timeout_ns >= 0) {
+               ts.tv_sec = timeout_ns / 1000000000ULL;
+               ts.tv_nsec = timeout_ns % 1000000000ULL;
+       }
+
+       ret = ppoll(&pfd, 1, timeout_ns < 0 ? NULL : &ts, NULL);
+       if (ret < 0)
+               return -1;
+       else if (ret == 0)
+               return 0;
+
+       return 1;
+}
+
+int gpiod_set_output_value(enum gpiod_line_value in, enum gpiod_line_value *out)
+{
+       switch (in) {
+       case GPIOD_LINE_VALUE_INACTIVE:
+       case GPIOD_LINE_VALUE_ACTIVE:
+               *out = in;
+               break;
+       default:
+               *out = GPIOD_LINE_VALUE_INACTIVE;
+               errno = EINVAL;
+               return -1;
+       }
+
+       return 0;
+}
+
+int gpiod_ioctl(int fd, unsigned long request, void *arg)
+{
+       int ret;
+
+       ret = ioctl(fd, request, arg);
+       if (ret <= 0)
+               return ret;
+
+       errno = EBADE;
+       return -1;
+}
+
+void gpiod_line_mask_zero(uint64_t *mask)
+{
+       *mask = 0ULL;
+}
+
+bool gpiod_line_mask_test_bit(const uint64_t *mask, int nr)
+{
+       return *mask & (1ULL << nr);
+}
+
+void gpiod_line_mask_set_bit(uint64_t *mask, unsigned int nr)
+{
+       *mask |= (1ULL << nr);
+}
+
+void gpiod_line_mask_assign_bit(uint64_t *mask, unsigned int nr, bool value)
+{
+       if (value)
+               gpiod_line_mask_set_bit(mask, nr);
+       else
+               *mask &= ~(1ULL << nr);
+}
diff --git a/lib/internal.h b/lib/internal.h
new file mode 100644 (file)
index 0000000..420fbdd
--- /dev/null
@@ -0,0 +1,48 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/* SPDX-FileCopyrightText: 2021 Bartosz Golaszewski <bgolaszewski@baylibre.com> */
+
+#ifndef __LIBGPIOD_GPIOD_INTERNAL_H__
+#define __LIBGPIOD_GPIOD_INTERNAL_H__
+
+#include <gpiod.h>
+#include <stddef.h>
+#include <stdint.h>
+
+#include "uapi/gpio.h"
+
+/* For internal library use only. */
+
+#define GPIOD_API      __attribute__((visibility("default")))
+#define GPIOD_BIT(nr)  (1UL << (nr))
+
+bool gpiod_check_gpiochip_device(const char *path, bool set_errno);
+
+struct gpiod_chip_info *
+gpiod_chip_info_from_uapi(struct gpiochip_info *uapi_info);
+struct gpiod_line_info *
+gpiod_line_info_from_uapi(struct gpio_v2_line_info *uapi_info);
+void gpiod_request_config_to_uapi(struct gpiod_request_config *config,
+                                 struct gpio_v2_line_request *uapi_req);
+int gpiod_line_config_to_uapi(struct gpiod_line_config *config,
+                             struct gpio_v2_line_request *uapi_cfg);
+struct gpiod_line_request *
+gpiod_line_request_from_uapi(struct gpio_v2_line_request *uapi_req,
+                            const char *chip_name);
+int gpiod_edge_event_buffer_read_fd(int fd,
+                                   struct gpiod_edge_event_buffer *buffer,
+                                   size_t max_events);
+struct gpiod_info_event *
+gpiod_info_event_from_uapi(struct gpio_v2_line_info_changed *uapi_evt);
+struct gpiod_info_event *gpiod_info_event_read_fd(int fd);
+
+int gpiod_poll_fd(int fd, int64_t timeout);
+int gpiod_set_output_value(enum gpiod_line_value in,
+                          enum gpiod_line_value *out);
+int gpiod_ioctl(int fd, unsigned long request, void *arg);
+
+void gpiod_line_mask_zero(uint64_t *mask);
+bool gpiod_line_mask_test_bit(const uint64_t *mask, int nr);
+void gpiod_line_mask_set_bit(uint64_t *mask, unsigned int nr);
+void gpiod_line_mask_assign_bit(uint64_t *mask, unsigned int nr, bool value);
+
+#endif /* __LIBGPIOD_GPIOD_INTERNAL_H__ */
diff --git a/lib/libgpiod.pc.in b/lib/libgpiod.pc.in
new file mode 100644 (file)
index 0000000..913859a
--- /dev/null
@@ -0,0 +1,15 @@
+# SPDX-License-Identifier: GPL-2.0-or-later
+# SPDX-FileCopyrightText: 2017-2021 Bartosz Golaszewski <bartekgola@gmail.com>
+# SPDX-FileCopyrightText: 2018 Clemens Gruber <clemens.gruber@pqgruber.com>
+
+prefix=@prefix@
+exec_prefix=@exec_prefix@
+libdir=@libdir@
+includedir=@includedir@
+
+Name: libgpiod
+Description: Library and tools for the Linux GPIO chardev
+URL: @PACKAGE_URL@
+Version: @PACKAGE_VERSION@
+Libs: -L${libdir} -lgpiod
+Cflags: -I${includedir}
diff --git a/lib/line-config.c b/lib/line-config.c
new file mode 100644 (file)
index 0000000..9302c1b
--- /dev/null
@@ -0,0 +1,549 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// SPDX-FileCopyrightText: 2022 Bartosz Golaszewski <brgl@bgdev.pl>
+
+#include <assert.h>
+#include <errno.h>
+#include <gpiod.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/param.h>
+
+#include "internal.h"
+
+#define LINES_MAX (GPIO_V2_LINES_MAX)
+
+struct settings_node {
+       struct settings_node *next;
+       struct gpiod_line_settings *settings;
+};
+
+struct per_line_config {
+       unsigned int offset;
+       struct settings_node *node;
+};
+
+struct gpiod_line_config {
+       struct per_line_config line_configs[LINES_MAX];
+       size_t num_configs;
+       enum gpiod_line_value output_values[LINES_MAX];
+       size_t num_output_values;
+       struct settings_node *sref_list;
+};
+
+GPIOD_API struct gpiod_line_config *gpiod_line_config_new(void)
+{
+       struct gpiod_line_config *config;
+
+       config = malloc(sizeof(*config));
+       if (!config)
+               return NULL;
+
+       memset(config, 0, sizeof(*config));
+
+       return config;
+}
+
+static void free_refs(struct gpiod_line_config *config)
+{
+       struct settings_node *node, *tmp;
+
+       for (node = config->sref_list; node;) {
+               tmp = node->next;
+               gpiod_line_settings_free(node->settings);
+               free(node);
+               node = tmp;
+       }
+}
+
+GPIOD_API void gpiod_line_config_free(struct gpiod_line_config *config)
+{
+       if (!config)
+               return;
+
+       free_refs(config);
+       free(config);
+}
+
+GPIOD_API void gpiod_line_config_reset(struct gpiod_line_config *config)
+{
+       assert(config);
+
+       free_refs(config);
+       memset(config, 0, sizeof(*config));
+}
+
+static struct per_line_config *find_config(struct gpiod_line_config *config,
+                                          unsigned int offset)
+{
+       struct per_line_config *per_line;
+       size_t i;
+
+       for (i = 0; i < config->num_configs; i++) {
+               per_line = &config->line_configs[i];
+
+               if (offset == per_line->offset)
+                       return per_line;
+       }
+
+       return &config->line_configs[config->num_configs++];
+}
+
+GPIOD_API int gpiod_line_config_add_line_settings(
+       struct gpiod_line_config *config, const unsigned int *offsets,
+       size_t num_offsets, struct gpiod_line_settings *settings)
+{
+       struct per_line_config *per_line;
+       struct settings_node *node;
+       size_t i;
+
+       assert(config);
+
+       if (!offsets || num_offsets == 0) {
+               errno = EINVAL;
+               return -1;
+       }
+
+       if ((config->num_configs + num_offsets) > LINES_MAX) {
+               errno = E2BIG;
+               return -1;
+       }
+
+       node = malloc(sizeof(*node));
+       if (!node)
+               return -1;
+
+       if (!settings)
+               node->settings = gpiod_line_settings_new();
+       else
+               node->settings = gpiod_line_settings_copy(settings);
+       if (!node->settings) {
+               free(node);
+               return -1;
+       }
+
+       node->next = config->sref_list;
+       config->sref_list = node;
+
+       for (i = 0; i < num_offsets; i++) {
+               per_line = find_config(config, offsets[i]);
+
+               per_line->offset = offsets[i];
+               per_line->node = node;
+       }
+
+       return 0;
+}
+
+GPIOD_API struct gpiod_line_settings *
+gpiod_line_config_get_line_settings(struct gpiod_line_config *config,
+                                   unsigned int offset)
+{
+       struct gpiod_line_settings *settings;
+       struct per_line_config *per_line;
+       size_t i;
+       int ret;
+
+       assert(config);
+
+       for (i = 0; i < config->num_configs; i++) {
+               per_line = &config->line_configs[i];
+
+               if (per_line->offset == offset) {
+                       settings = gpiod_line_settings_copy(
+                                       per_line->node->settings);
+                       if (!settings)
+                               return NULL;
+
+                       /*
+                        * If a global output value was set for this line - use
+                        * it and override the one stored in settings.
+                        */
+                       if (config->num_output_values > i) {
+                               ret = gpiod_line_settings_set_output_value(
+                                               settings,
+                                               config->output_values[i]);
+                               if (ret) {
+                                       gpiod_line_settings_free(settings);
+                                       return NULL;
+                               }
+                       }
+
+                       return settings;
+               }
+       }
+
+       errno = ENOENT;
+       return NULL;
+}
+
+GPIOD_API int
+gpiod_line_config_set_output_values(struct gpiod_line_config *config,
+                                   const enum gpiod_line_value *values,
+                                   size_t num_values)
+{
+       size_t i;
+       int ret;
+
+       assert(config);
+
+       if (!num_values || num_values > LINES_MAX || !values) {
+               errno = EINVAL;
+               return -1;
+       }
+
+       for (i = 0; i < num_values; i++) {
+               ret = gpiod_set_output_value(values[i],
+                                            &config->output_values[i]);
+               if (ret) {
+                       config->num_output_values = 0;
+                       return ret;
+               }
+       }
+
+       config->num_output_values = num_values;
+
+       return 0;
+}
+
+GPIOD_API size_t
+gpiod_line_config_get_num_configured_offsets(struct gpiod_line_config *config)
+{
+       assert(config);
+
+       return config->num_configs;
+}
+
+GPIOD_API size_t
+gpiod_line_config_get_configured_offsets(struct gpiod_line_config *config,
+                                        unsigned int *offsets,
+                                        size_t max_offsets)
+{
+       size_t num_offsets, i;
+
+       assert(config);
+
+       if (!offsets || !max_offsets || !config->num_configs)
+               return 0;
+
+       num_offsets = MIN(config->num_configs, max_offsets);
+
+       for (i = 0; i < num_offsets; i++)
+               offsets[i] = config->line_configs[i].offset;
+
+       return num_offsets;
+}
+
+static void set_offsets(struct gpiod_line_config *config,
+                       struct gpio_v2_line_request *uapi_cfg)
+{
+       size_t i;
+
+       uapi_cfg->num_lines = config->num_configs;
+
+       for (i = 0; i < config->num_configs; i++)
+               uapi_cfg->offsets[i] = config->line_configs[i].offset;
+}
+
+static bool has_at_least_one_output_direction(struct gpiod_line_config *config)
+{
+       size_t i;
+
+       for (i = 0; i < config->num_configs; i++) {
+               if (gpiod_line_settings_get_direction(
+                           config->line_configs[i].node->settings) ==
+                   GPIOD_LINE_DIRECTION_OUTPUT)
+                       return true;
+       }
+
+       return false;
+}
+
+static void set_output_value(uint64_t *vals, size_t bit,
+                            enum gpiod_line_value value)
+{
+       gpiod_line_mask_assign_bit(vals, bit,
+                                  value == GPIOD_LINE_VALUE_ACTIVE ? 1 : 0);
+}
+
+static void set_kernel_output_values(uint64_t *mask, uint64_t *vals,
+                                    struct gpiod_line_config *config)
+{
+       struct per_line_config *per_line;
+       enum gpiod_line_value value;
+       size_t i;
+
+       gpiod_line_mask_zero(mask);
+       gpiod_line_mask_zero(vals);
+
+       for (i = 0; i < config->num_configs; i++) {
+               per_line = &config->line_configs[i];
+
+               if (gpiod_line_settings_get_direction(
+                           per_line->node->settings) !=
+                   GPIOD_LINE_DIRECTION_OUTPUT)
+                       continue;
+
+               gpiod_line_mask_set_bit(mask, i);
+               value = gpiod_line_settings_get_output_value(
+                       per_line->node->settings);
+               set_output_value(vals, i, value);
+       }
+
+       /* "Global" output values override the ones from per-line settings. */
+       for (i = 0; i < config->num_output_values; i++) {
+               gpiod_line_mask_set_bit(mask, i);
+               value = config->output_values[i];
+               set_output_value(vals, i, value);
+       }
+}
+
+static void set_output_values(struct gpiod_line_config *config,
+                             struct gpio_v2_line_request *uapi_cfg,
+                             unsigned int *attr_idx)
+{
+       struct gpio_v2_line_config_attribute *attr;
+       uint64_t mask, values;
+
+       if (!has_at_least_one_output_direction(config))
+               return;
+
+       attr = &uapi_cfg->config.attrs[(*attr_idx)++];
+       attr->attr.id = GPIO_V2_LINE_ATTR_ID_OUTPUT_VALUES;
+       set_kernel_output_values(&mask, &values, config);
+       attr->attr.values = values;
+       attr->mask = mask;
+}
+
+static int set_debounce_periods(struct gpiod_line_config *config,
+                               struct gpio_v2_line_config *uapi_cfg,
+                               unsigned int *attr_idx)
+{
+       struct gpio_v2_line_config_attribute *attr;
+       unsigned long period_i, period_j;
+       uint64_t done, mask;
+       size_t i, j;
+
+       gpiod_line_mask_zero(&done);
+
+       for (i = 0; i < config->num_configs; i++) {
+               if (gpiod_line_mask_test_bit(&done, i))
+                       continue;
+
+               gpiod_line_mask_set_bit(&done, i);
+               gpiod_line_mask_zero(&mask);
+
+               period_i = gpiod_line_settings_get_debounce_period_us(
+                               config->line_configs[i].node->settings);
+               if (!period_i)
+                       continue;
+
+               if (*attr_idx == GPIO_V2_LINE_NUM_ATTRS_MAX) {
+                       errno = E2BIG;
+                       return -1;
+               }
+
+               attr = &uapi_cfg->attrs[(*attr_idx)++];
+               attr->attr.id = GPIO_V2_LINE_ATTR_ID_DEBOUNCE;
+               attr->attr.debounce_period_us = period_i;
+               gpiod_line_mask_set_bit(&mask, i);
+
+               for (j = i; j < config->num_configs; j++) {
+                       period_j = gpiod_line_settings_get_debounce_period_us(
+                                       config->line_configs[j].node->settings);
+                       if (period_i == period_j) {
+                               gpiod_line_mask_set_bit(&mask, j);
+                               gpiod_line_mask_set_bit(&done, j);
+                       }
+               }
+
+               attr->mask = mask;
+       }
+
+       return 0;
+}
+
+static uint64_t make_kernel_flags(struct gpiod_line_settings *settings)
+{
+       uint64_t flags = 0;
+
+       switch (gpiod_line_settings_get_direction(settings)) {
+       case GPIOD_LINE_DIRECTION_INPUT:
+               flags |= GPIO_V2_LINE_FLAG_INPUT;
+               break;
+       case GPIOD_LINE_DIRECTION_OUTPUT:
+               flags |= GPIO_V2_LINE_FLAG_OUTPUT;
+               break;
+       default:
+               break;
+       }
+
+       switch (gpiod_line_settings_get_edge_detection(settings)) {
+       case GPIOD_LINE_EDGE_FALLING:
+               flags |= (GPIO_V2_LINE_FLAG_EDGE_FALLING |
+                         GPIO_V2_LINE_FLAG_INPUT);
+               break;
+       case GPIOD_LINE_EDGE_RISING:
+               flags |= (GPIO_V2_LINE_FLAG_EDGE_RISING |
+                         GPIO_V2_LINE_FLAG_INPUT);
+               break;
+       case GPIOD_LINE_EDGE_BOTH:
+               flags |= (GPIO_V2_LINE_FLAG_EDGE_FALLING |
+                         GPIO_V2_LINE_FLAG_EDGE_RISING |
+                         GPIO_V2_LINE_FLAG_INPUT);
+               break;
+       default:
+               break;
+       }
+
+       switch (gpiod_line_settings_get_drive(settings)) {
+       case GPIOD_LINE_DRIVE_OPEN_DRAIN:
+               flags |= GPIO_V2_LINE_FLAG_OPEN_DRAIN;
+               break;
+       case GPIOD_LINE_DRIVE_OPEN_SOURCE:
+               flags |= GPIO_V2_LINE_FLAG_OPEN_SOURCE;
+               break;
+       default:
+               break;
+       }
+
+       switch (gpiod_line_settings_get_bias(settings)) {
+       case GPIOD_LINE_BIAS_DISABLED:
+               flags |= GPIO_V2_LINE_FLAG_BIAS_DISABLED;
+               break;
+       case GPIOD_LINE_BIAS_PULL_UP:
+               flags |= GPIO_V2_LINE_FLAG_BIAS_PULL_UP;
+               break;
+       case GPIOD_LINE_BIAS_PULL_DOWN:
+               flags |= GPIO_V2_LINE_FLAG_BIAS_PULL_DOWN;
+               break;
+       default:
+               break;
+       }
+
+       if (gpiod_line_settings_get_active_low(settings))
+               flags |= GPIO_V2_LINE_FLAG_ACTIVE_LOW;
+
+       switch (gpiod_line_settings_get_event_clock(settings)) {
+       case GPIOD_LINE_CLOCK_REALTIME:
+               flags |= GPIO_V2_LINE_FLAG_EVENT_CLOCK_REALTIME;
+               break;
+       case GPIOD_LINE_CLOCK_HTE:
+               flags |= GPIO_V2_LINE_FLAG_EVENT_CLOCK_HTE;
+               break;
+       default:
+               break;
+       }
+
+       return flags;
+}
+
+static bool settings_equal(struct gpiod_line_settings *left,
+                          struct gpiod_line_settings *right)
+{
+       if (gpiod_line_settings_get_direction(left) !=
+           gpiod_line_settings_get_direction(right))
+               return false;
+
+       if (gpiod_line_settings_get_edge_detection(left) !=
+           gpiod_line_settings_get_edge_detection(right))
+               return false;
+
+       if (gpiod_line_settings_get_bias(left) !=
+           gpiod_line_settings_get_bias(right))
+               return false;
+
+       if (gpiod_line_settings_get_drive(left) !=
+           gpiod_line_settings_get_drive(right))
+               return false;
+
+       if (gpiod_line_settings_get_active_low(left) !=
+           gpiod_line_settings_get_active_low(right))
+               return false;
+
+       if (gpiod_line_settings_get_event_clock(left) !=
+           gpiod_line_settings_get_event_clock(right))
+               return false;
+
+       return true;
+}
+
+static int set_flags(struct gpiod_line_config *config,
+                    struct gpio_v2_line_config *uapi_cfg,
+                    unsigned int *attr_idx)
+{
+       struct gpiod_line_settings *settings_i, *settings_j;
+       struct gpio_v2_line_config_attribute *attr;
+       bool globals_taken = false;
+       uint64_t done, mask;
+       size_t i, j;
+
+       gpiod_line_mask_zero(&done);
+
+       for (i = 0; i < config->num_configs; i++) {
+               if (gpiod_line_mask_test_bit(&done, i))
+                       continue;
+
+               gpiod_line_mask_set_bit(&done, i);
+
+               settings_i = config->line_configs[i].node->settings;
+
+               if (!globals_taken) {
+                       globals_taken = true;
+                       uapi_cfg->flags = make_kernel_flags(settings_i);
+
+                       for (j = i; j < config->num_configs; j++) {
+                               settings_j =
+                                       config->line_configs[j].node->settings;
+                               if (settings_equal(settings_i, settings_j))
+                                       gpiod_line_mask_set_bit(&done, j);
+                       }
+               } else {
+                       gpiod_line_mask_zero(&mask);
+                       gpiod_line_mask_set_bit(&mask, i);
+
+                       if (*attr_idx == GPIO_V2_LINE_NUM_ATTRS_MAX) {
+                               errno = E2BIG;
+                               return -1;
+                       }
+
+                       attr = &uapi_cfg->attrs[(*attr_idx)++];
+                       attr->attr.id = GPIO_V2_LINE_ATTR_ID_FLAGS;
+                       attr->attr.flags = make_kernel_flags(settings_i);
+
+                       for (j = i; j < config->num_configs; j++) {
+                               settings_j =
+                                       config->line_configs[j].node->settings;
+                               if (settings_equal(settings_i, settings_j)) {
+                                       gpiod_line_mask_set_bit(&done, j);
+                                       gpiod_line_mask_set_bit(&mask, j);
+                               }
+                       }
+
+                       attr->mask = mask;
+               }
+       }
+
+       return 0;
+}
+
+int gpiod_line_config_to_uapi(struct gpiod_line_config *config,
+                             struct gpio_v2_line_request *uapi_cfg)
+{
+       unsigned int attr_idx = 0;
+       int ret;
+
+       set_offsets(config, uapi_cfg);
+       set_output_values(config, uapi_cfg, &attr_idx);
+
+       ret = set_debounce_periods(config, &uapi_cfg->config, &attr_idx);
+       if (ret)
+               return -1;
+
+       ret = set_flags(config, &uapi_cfg->config, &attr_idx);
+       if (ret)
+               return -1;
+
+       uapi_cfg->config.num_attrs = attr_idx;
+
+       return 0;
+}
diff --git a/lib/line-info.c b/lib/line-info.c
new file mode 100644 (file)
index 0000000..c61b789
--- /dev/null
@@ -0,0 +1,211 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// SPDX-FileCopyrightText: 2021 Bartosz Golaszewski <brgl@bgdev.pl>
+
+#include <assert.h>
+#include <gpiod.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "internal.h"
+
+struct gpiod_line_info {
+       unsigned int offset;
+       char name[GPIO_MAX_NAME_SIZE + 1];
+       bool used;
+       char consumer[GPIO_MAX_NAME_SIZE + 1];
+       enum gpiod_line_direction direction;
+       bool active_low;
+       enum gpiod_line_bias bias;
+       enum gpiod_line_drive drive;
+       enum gpiod_line_edge edge;
+       enum gpiod_line_clock event_clock;
+       bool debounced;
+       unsigned long debounce_period_us;
+};
+
+GPIOD_API void gpiod_line_info_free(struct gpiod_line_info *info)
+{
+       free(info);
+}
+
+GPIOD_API struct gpiod_line_info *
+gpiod_line_info_copy(struct gpiod_line_info *info)
+{
+       struct gpiod_line_info *copy;
+
+       assert(info);
+
+       copy = malloc(sizeof(*info));
+       if (!copy)
+               return NULL;
+
+       memcpy(copy, info, sizeof(*info));
+
+       return copy;
+}
+
+GPIOD_API unsigned int gpiod_line_info_get_offset(struct gpiod_line_info *info)
+{
+       assert(info);
+
+       return info->offset;
+}
+
+GPIOD_API const char *gpiod_line_info_get_name(struct gpiod_line_info *info)
+{
+       assert(info);
+
+       return info->name[0] == '\0' ? NULL : info->name;
+}
+
+GPIOD_API bool gpiod_line_info_is_used(struct gpiod_line_info *info)
+{
+       assert(info);
+
+       return info->used;
+}
+
+GPIOD_API const char *gpiod_line_info_get_consumer(struct gpiod_line_info *info)
+{
+       assert(info);
+
+       return info->consumer[0] == '\0' ? NULL : info->consumer;
+}
+
+GPIOD_API enum gpiod_line_direction
+gpiod_line_info_get_direction(struct gpiod_line_info *info)
+{
+       assert(info);
+
+       return info->direction;
+}
+
+GPIOD_API bool gpiod_line_info_is_active_low(struct gpiod_line_info *info)
+{
+       assert(info);
+
+       return info->active_low;
+}
+
+GPIOD_API enum gpiod_line_bias
+gpiod_line_info_get_bias(struct gpiod_line_info *info)
+{
+       assert(info);
+
+       return info->bias;
+}
+
+GPIOD_API enum gpiod_line_drive
+gpiod_line_info_get_drive(struct gpiod_line_info *info)
+{
+       assert(info);
+
+       return info->drive;
+}
+
+GPIOD_API enum gpiod_line_edge
+gpiod_line_info_get_edge_detection(struct gpiod_line_info *info)
+{
+       assert(info);
+
+       return info->edge;
+}
+
+GPIOD_API enum gpiod_line_clock
+gpiod_line_info_get_event_clock(struct gpiod_line_info *info)
+{
+       assert(info);
+
+       return info->event_clock;
+}
+
+GPIOD_API bool gpiod_line_info_is_debounced(struct gpiod_line_info *info)
+{
+       assert(info);
+
+       return info->debounced;
+}
+
+GPIOD_API unsigned long
+gpiod_line_info_get_debounce_period_us(struct gpiod_line_info *info)
+{
+       assert(info);
+
+       return info->debounce_period_us;
+}
+
+struct gpiod_line_info *
+gpiod_line_info_from_uapi(struct gpio_v2_line_info *uapi_info)
+{
+       struct gpio_v2_line_attribute *attr;
+       struct gpiod_line_info *info;
+       size_t i;
+
+       info = malloc(sizeof(*info));
+       if (!info)
+               return NULL;
+
+       memset(info, 0, sizeof(*info));
+
+       info->offset = uapi_info->offset;
+       strncpy(info->name, uapi_info->name, GPIO_MAX_NAME_SIZE);
+
+       info->used = !!(uapi_info->flags & GPIO_V2_LINE_FLAG_USED);
+       strncpy(info->consumer, uapi_info->consumer, GPIO_MAX_NAME_SIZE);
+
+       if (uapi_info->flags & GPIO_V2_LINE_FLAG_OUTPUT)
+               info->direction = GPIOD_LINE_DIRECTION_OUTPUT;
+       else
+               info->direction = GPIOD_LINE_DIRECTION_INPUT;
+
+       if (uapi_info->flags & GPIO_V2_LINE_FLAG_ACTIVE_LOW)
+               info->active_low = true;
+
+       if (uapi_info->flags & GPIO_V2_LINE_FLAG_BIAS_PULL_UP)
+               info->bias = GPIOD_LINE_BIAS_PULL_UP;
+       else if (uapi_info->flags & GPIO_V2_LINE_FLAG_BIAS_PULL_DOWN)
+               info->bias = GPIOD_LINE_BIAS_PULL_DOWN;
+       else if (uapi_info->flags & GPIO_V2_LINE_FLAG_BIAS_DISABLED)
+               info->bias = GPIOD_LINE_BIAS_DISABLED;
+       else
+               info->bias = GPIOD_LINE_BIAS_UNKNOWN;
+
+       if (uapi_info->flags & GPIO_V2_LINE_FLAG_OPEN_DRAIN)
+               info->drive = GPIOD_LINE_DRIVE_OPEN_DRAIN;
+       else if (uapi_info->flags & GPIO_V2_LINE_FLAG_OPEN_SOURCE)
+               info->drive = GPIOD_LINE_DRIVE_OPEN_SOURCE;
+       else
+               info->drive = GPIOD_LINE_DRIVE_PUSH_PULL;
+
+       if ((uapi_info->flags & GPIO_V2_LINE_FLAG_EDGE_RISING) &&
+           (uapi_info->flags & GPIO_V2_LINE_FLAG_EDGE_FALLING))
+               info->edge = GPIOD_LINE_EDGE_BOTH;
+       else if (uapi_info->flags & GPIO_V2_LINE_FLAG_EDGE_RISING)
+               info->edge = GPIOD_LINE_EDGE_RISING;
+       else if (uapi_info->flags & GPIO_V2_LINE_FLAG_EDGE_FALLING)
+               info->edge = GPIOD_LINE_EDGE_FALLING;
+       else
+               info->edge = GPIOD_LINE_EDGE_NONE;
+
+       if (uapi_info->flags & GPIO_V2_LINE_FLAG_EVENT_CLOCK_REALTIME)
+               info->event_clock = GPIOD_LINE_CLOCK_REALTIME;
+       else if (uapi_info->flags & GPIO_V2_LINE_FLAG_EVENT_CLOCK_HTE)
+               info->event_clock = GPIOD_LINE_CLOCK_HTE;
+       else
+               info->event_clock = GPIOD_LINE_CLOCK_MONOTONIC;
+
+       /*
+        * We assume that the kernel returns correct configuration and that no
+        * attributes repeat.
+        */
+       for (i = 0; i < uapi_info->num_attrs; i++) {
+               attr = &uapi_info->attrs[i];
+
+               if (attr->id == GPIO_V2_LINE_ATTR_ID_DEBOUNCE) {
+                       info->debounced = true;
+                       info->debounce_period_us = attr->debounce_period_us;
+               }
+       }
+
+       return info;
+}
diff --git a/lib/line-request.c b/lib/line-request.c
new file mode 100644 (file)
index 0000000..b76b3d7
--- /dev/null
@@ -0,0 +1,307 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// SPDX-FileCopyrightText: 2021 Bartosz Golaszewski <brgl@bgdev.pl>
+
+#include <assert.h>
+#include <errno.h>
+#include <gpiod.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/param.h>
+#include <unistd.h>
+
+#include "internal.h"
+
+struct gpiod_line_request {
+       char *chip_name;
+       unsigned int offsets[GPIO_V2_LINES_MAX];
+       size_t num_lines;
+       int fd;
+};
+
+struct gpiod_line_request *
+gpiod_line_request_from_uapi(struct gpio_v2_line_request *uapi_req,
+                            const char *chip_name)
+{
+       struct gpiod_line_request *request;
+
+       request = malloc(sizeof(*request));
+       if (!request)
+               return NULL;
+
+       memset(request, 0, sizeof(*request));
+
+       request->chip_name = strdup(chip_name);
+       if (!request->chip_name) {
+               free(request);
+               return NULL;
+       }
+
+       request->fd = uapi_req->fd;
+       request->num_lines = uapi_req->num_lines;
+       memcpy(request->offsets, uapi_req->offsets,
+              sizeof(*request->offsets) * request->num_lines);
+
+       return request;
+}
+
+GPIOD_API void gpiod_line_request_release(struct gpiod_line_request *request)
+{
+       if (!request)
+               return;
+
+       close(request->fd);
+       free(request->chip_name);
+       free(request);
+}
+
+GPIOD_API const char *
+gpiod_line_request_get_chip_name(struct gpiod_line_request *request)
+{
+       assert(request);
+
+       return request->chip_name;
+}
+
+GPIOD_API size_t
+gpiod_line_request_get_num_requested_lines(struct gpiod_line_request *request)
+{
+       assert(request);
+
+       return request->num_lines;
+}
+
+GPIOD_API size_t
+gpiod_line_request_get_requested_offsets(struct gpiod_line_request *request,
+                                        unsigned int *offsets,
+                                        size_t max_offsets)
+{
+       size_t num_offsets;
+
+       assert(request);
+
+       if (!offsets || !max_offsets)
+               return 0;
+
+       num_offsets = MIN(request->num_lines, max_offsets);
+
+       memcpy(offsets, request->offsets, sizeof(*offsets) * num_offsets);
+
+       return num_offsets;
+}
+
+GPIOD_API enum gpiod_line_value
+gpiod_line_request_get_value(struct gpiod_line_request *request,
+                            unsigned int offset)
+{
+       enum gpiod_line_value val;
+       unsigned int ret;
+
+       assert(request);
+
+       ret = gpiod_line_request_get_values_subset(request, 1, &offset, &val);
+       if (ret)
+               return GPIOD_LINE_VALUE_ERROR;
+
+       return val;
+}
+
+static int offset_to_bit(struct gpiod_line_request *request,
+                        unsigned int offset)
+{
+       size_t i;
+
+       assert(request);
+
+       for (i = 0; i < request->num_lines; i++) {
+               if (offset == request->offsets[i])
+                       return i;
+       }
+
+       return -1;
+}
+
+GPIOD_API int
+gpiod_line_request_get_values_subset(struct gpiod_line_request *request,
+                                    size_t num_values,
+                                    const unsigned int *offsets,
+                                    enum gpiod_line_value *values)
+{
+       struct gpio_v2_line_values uapi_values;
+       uint64_t mask = 0, bits = 0;
+       size_t i;
+       int bit, ret;
+
+       assert(request);
+
+       if (!offsets || !values) {
+               errno = EINVAL;
+               return -1;
+       }
+
+       uapi_values.bits = 0;
+
+       for (i = 0; i < num_values; i++) {
+               bit = offset_to_bit(request, offsets[i]);
+               if (bit < 0) {
+                       errno = EINVAL;
+                       return -1;
+               }
+
+               gpiod_line_mask_set_bit(&mask, bit);
+       }
+
+       uapi_values.mask = mask;
+
+       ret = gpiod_ioctl(request->fd, GPIO_V2_LINE_GET_VALUES_IOCTL,
+                         &uapi_values);
+       if (ret)
+               return -1;
+
+       bits = uapi_values.bits;
+       memset(values, 0, sizeof(*values) * num_values);
+
+       for (i = 0; i < num_values; i++) {
+               bit = offset_to_bit(request, offsets[i]);
+               values[i] = gpiod_line_mask_test_bit(&bits, bit) ? 1 : 0;
+       }
+
+       return 0;
+}
+
+GPIOD_API int gpiod_line_request_get_values(struct gpiod_line_request *request,
+                                           enum gpiod_line_value *values)
+{
+       assert(request);
+
+       return gpiod_line_request_get_values_subset(request, request->num_lines,
+                                                   request->offsets, values);
+}
+
+GPIOD_API int gpiod_line_request_set_value(struct gpiod_line_request *request,
+                                          unsigned int offset,
+                                          enum gpiod_line_value value)
+{
+       return gpiod_line_request_set_values_subset(request, 1,
+                                                   &offset, &value);
+}
+
+GPIOD_API int
+gpiod_line_request_set_values_subset(struct gpiod_line_request *request,
+                                    size_t num_values,
+                                    const unsigned int *offsets,
+                                    const enum gpiod_line_value *values)
+{
+       struct gpio_v2_line_values uapi_values;
+       uint64_t mask = 0, bits = 0;
+       size_t i;
+       int bit;
+
+       assert(request);
+
+       if (!offsets || !values) {
+               errno = EINVAL;
+               return -1;
+       }
+
+       for (i = 0; i < num_values; i++) {
+               bit = offset_to_bit(request, offsets[i]);
+               if (bit < 0) {
+                       errno = EINVAL;
+                       return -1;
+               }
+
+               gpiod_line_mask_set_bit(&mask, bit);
+               gpiod_line_mask_assign_bit(&bits, bit, values[i]);
+       }
+
+       memset(&uapi_values, 0, sizeof(uapi_values));
+       uapi_values.mask = mask;
+       uapi_values.bits = bits;
+
+       return gpiod_ioctl(request->fd, GPIO_V2_LINE_SET_VALUES_IOCTL,
+                          &uapi_values);
+}
+
+GPIOD_API int gpiod_line_request_set_values(struct gpiod_line_request *request,
+                                           const enum gpiod_line_value *values)
+{
+       assert(request);
+
+       return gpiod_line_request_set_values_subset(request, request->num_lines,
+                                                   request->offsets, values);
+}
+
+static bool offsets_equal(struct gpiod_line_request *request,
+                         struct gpio_v2_line_request *uapi_cfg)
+{
+       size_t i;
+
+       if (request->num_lines != uapi_cfg->num_lines)
+               return false;
+
+       for (i = 0; i < request->num_lines; i++) {
+               if (request->offsets[i] != uapi_cfg->offsets[i])
+                       return false;
+       }
+
+       return true;
+}
+
+GPIOD_API int
+gpiod_line_request_reconfigure_lines(struct gpiod_line_request *request,
+                                    struct gpiod_line_config *config)
+{
+       struct gpio_v2_line_request uapi_cfg;
+       int ret;
+
+       assert(request);
+
+       if (!config) {
+               errno = EINVAL;
+               return -1;
+       }
+
+       memset(&uapi_cfg, 0, sizeof(uapi_cfg));
+
+       ret = gpiod_line_config_to_uapi(config, &uapi_cfg);
+       if (ret)
+               return ret;
+
+       if (!offsets_equal(request, &uapi_cfg)) {
+               errno = EINVAL;
+               return -1;
+       }
+
+       ret = gpiod_ioctl(request->fd, GPIO_V2_LINE_SET_CONFIG_IOCTL,
+                         &uapi_cfg.config);
+       if (ret)
+               return ret;
+
+       return 0;
+}
+
+GPIOD_API int gpiod_line_request_get_fd(struct gpiod_line_request *request)
+{
+       assert(request);
+
+       return request->fd;
+}
+
+GPIOD_API int
+gpiod_line_request_wait_edge_events(struct gpiod_line_request *request,
+                                   int64_t timeout_ns)
+{
+       assert(request);
+
+       return gpiod_poll_fd(request->fd, timeout_ns);
+}
+
+GPIOD_API int
+gpiod_line_request_read_edge_events(struct gpiod_line_request *request,
+                                   struct gpiod_edge_event_buffer *buffer,
+                                   size_t max_events)
+{
+       assert(request);
+
+       return gpiod_edge_event_buffer_read_fd(request->fd, buffer, max_events);
+}
diff --git a/lib/line-settings.c b/lib/line-settings.c
new file mode 100644 (file)
index 0000000..e54353f
--- /dev/null
@@ -0,0 +1,266 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// SPDX-FileCopyrightText: 2022 Bartosz Golaszewski <brgl@bgdev.pl>
+
+#include <assert.h>
+#include <errno.h>
+#include <gpiod.h>
+#include <string.h>
+#include <stdlib.h>
+
+#include "internal.h"
+
+struct gpiod_line_settings {
+       enum gpiod_line_direction direction;
+       enum gpiod_line_edge edge_detection;
+       enum gpiod_line_drive drive;
+       enum gpiod_line_bias bias;
+       bool active_low;
+       enum gpiod_line_clock event_clock;
+       long debounce_period_us;
+       enum gpiod_line_value output_value;
+};
+
+GPIOD_API struct gpiod_line_settings *gpiod_line_settings_new(void)
+{
+       struct gpiod_line_settings *settings;
+
+       settings = malloc(sizeof(*settings));
+       if (!settings)
+               return NULL;
+
+       gpiod_line_settings_reset(settings);
+
+       return settings;
+}
+
+GPIOD_API void gpiod_line_settings_free(struct gpiod_line_settings *settings)
+{
+       free(settings);
+}
+
+GPIOD_API void gpiod_line_settings_reset(struct gpiod_line_settings *settings)
+{
+       assert(settings);
+
+       settings->direction = GPIOD_LINE_DIRECTION_AS_IS;
+       settings->edge_detection = GPIOD_LINE_EDGE_NONE;
+       settings->bias = GPIOD_LINE_BIAS_AS_IS;
+       settings->drive = GPIOD_LINE_DRIVE_PUSH_PULL;
+       settings->active_low = false;
+       settings->debounce_period_us = 0;
+       settings->event_clock = GPIOD_LINE_CLOCK_MONOTONIC;
+       settings->output_value = GPIOD_LINE_VALUE_INACTIVE;
+}
+
+GPIOD_API struct gpiod_line_settings *
+gpiod_line_settings_copy(struct gpiod_line_settings *settings)
+{
+       assert(settings);
+
+       struct gpiod_line_settings *copy;
+
+       copy = malloc(sizeof(*copy));
+       if (!copy)
+               return NULL;
+
+       memcpy(copy, settings, sizeof(*copy));
+
+       return copy;
+}
+
+GPIOD_API int
+gpiod_line_settings_set_direction(struct gpiod_line_settings *settings,
+                                 enum gpiod_line_direction direction)
+{
+       assert(settings);
+
+       switch (direction) {
+       case GPIOD_LINE_DIRECTION_INPUT:
+       case GPIOD_LINE_DIRECTION_OUTPUT:
+       case GPIOD_LINE_DIRECTION_AS_IS:
+               settings->direction = direction;
+               break;
+       default:
+               settings->direction = GPIOD_LINE_DIRECTION_AS_IS;
+               errno = EINVAL;
+               return -1;
+       }
+
+       return 0;
+}
+
+GPIOD_API enum gpiod_line_direction
+gpiod_line_settings_get_direction(struct gpiod_line_settings *settings)
+{
+       assert(settings);
+
+       return settings->direction;
+}
+
+GPIOD_API int
+gpiod_line_settings_set_edge_detection(struct gpiod_line_settings *settings,
+                                      enum gpiod_line_edge edge)
+{
+       assert(settings);
+
+       switch (edge) {
+       case GPIOD_LINE_EDGE_NONE:
+       case GPIOD_LINE_EDGE_RISING:
+       case GPIOD_LINE_EDGE_FALLING:
+       case GPIOD_LINE_EDGE_BOTH:
+               settings->edge_detection = edge;
+               break;
+       default:
+               settings->edge_detection = GPIOD_LINE_EDGE_NONE;
+               errno = EINVAL;
+               return -1;
+       }
+
+       return 0;
+}
+
+GPIOD_API enum gpiod_line_edge
+gpiod_line_settings_get_edge_detection(struct gpiod_line_settings *settings)
+{
+       assert(settings);
+
+       return settings->edge_detection;
+}
+
+GPIOD_API int gpiod_line_settings_set_bias(struct gpiod_line_settings *settings,
+                                          enum gpiod_line_bias bias)
+{
+       assert(settings);
+
+       switch (bias) {
+       case GPIOD_LINE_BIAS_AS_IS:
+       case GPIOD_LINE_BIAS_DISABLED:
+       case GPIOD_LINE_BIAS_PULL_UP:
+       case GPIOD_LINE_BIAS_PULL_DOWN:
+               settings->bias = bias;
+               break;
+       default:
+               settings->bias = GPIOD_LINE_BIAS_AS_IS;
+               errno = EINVAL;
+               return -1;
+       }
+
+       return 0;
+}
+
+GPIOD_API enum gpiod_line_bias
+gpiod_line_settings_get_bias(struct gpiod_line_settings *settings)
+{
+       assert(settings);
+
+       return settings->bias;
+}
+
+GPIOD_API int
+gpiod_line_settings_set_drive(struct gpiod_line_settings *settings,
+                             enum gpiod_line_drive drive)
+{
+       assert(settings);
+
+       switch (drive) {
+       case GPIOD_LINE_DRIVE_PUSH_PULL:
+       case GPIOD_LINE_DRIVE_OPEN_DRAIN:
+       case GPIOD_LINE_DRIVE_OPEN_SOURCE:
+               settings->drive = drive;
+               break;
+       default:
+               settings->drive = GPIOD_LINE_DRIVE_PUSH_PULL;
+               errno = EINVAL;
+               return -1;
+       }
+
+       return 0;
+}
+
+GPIOD_API enum gpiod_line_drive
+gpiod_line_settings_get_drive(struct gpiod_line_settings *settings)
+{
+       assert(settings);
+
+       return settings->drive;
+}
+
+GPIOD_API void
+gpiod_line_settings_set_active_low(struct gpiod_line_settings *settings,
+                                  bool active_low)
+{
+       assert(settings);
+
+       settings->active_low = active_low;
+}
+
+GPIOD_API bool
+gpiod_line_settings_get_active_low(struct gpiod_line_settings *settings)
+{
+       assert(settings);
+
+       return settings->active_low;
+}
+
+GPIOD_API void
+gpiod_line_settings_set_debounce_period_us(struct gpiod_line_settings *settings,
+                                          unsigned long period)
+{
+       assert(settings);
+
+       settings->debounce_period_us = period;
+}
+
+GPIOD_API unsigned long
+gpiod_line_settings_get_debounce_period_us(struct gpiod_line_settings *settings)
+{
+       assert(settings);
+
+       return settings->debounce_period_us;
+}
+
+GPIOD_API int
+gpiod_line_settings_set_event_clock(struct gpiod_line_settings *settings,
+                                   enum gpiod_line_clock event_clock)
+{
+       assert(settings);
+
+       switch (event_clock) {
+       case GPIOD_LINE_CLOCK_MONOTONIC:
+       case GPIOD_LINE_CLOCK_REALTIME:
+       case GPIOD_LINE_CLOCK_HTE:
+               settings->event_clock = event_clock;
+               break;
+       default:
+               settings->event_clock = GPIOD_LINE_CLOCK_MONOTONIC;
+               errno = EINVAL;
+               return -1;
+       }
+
+       return 0;
+}
+
+GPIOD_API enum gpiod_line_clock
+gpiod_line_settings_get_event_clock(struct gpiod_line_settings *settings)
+{
+       assert(settings);
+
+       return settings->event_clock;
+}
+
+GPIOD_API int
+gpiod_line_settings_set_output_value(struct gpiod_line_settings *settings,
+                                    enum gpiod_line_value value)
+{
+       assert(settings);
+
+       return gpiod_set_output_value(value, &settings->output_value);
+}
+
+GPIOD_API enum gpiod_line_value
+gpiod_line_settings_get_output_value(struct gpiod_line_settings *settings)
+{
+       assert(settings);
+
+       return settings->output_value;
+}
diff --git a/lib/misc.c b/lib/misc.c
new file mode 100644 (file)
index 0000000..e109f80
--- /dev/null
@@ -0,0 +1,16 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// SPDX-FileCopyrightText: 2017-2022 Bartosz Golaszewski <bartekgola@gmail.com>
+
+#include <gpiod.h>
+
+#include "internal.h"
+
+GPIOD_API bool gpiod_is_gpiochip_device(const char *path)
+{
+       return gpiod_check_gpiochip_device(path, false);
+}
+
+GPIOD_API const char *gpiod_api_version(void)
+{
+       return GPIOD_VERSION_STR;
+}
diff --git a/lib/request-config.c b/lib/request-config.c
new file mode 100644 (file)
index 0000000..da055c5
--- /dev/null
@@ -0,0 +1,79 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// SPDX-FileCopyrightText: 2021 Bartosz Golaszewski <brgl@bgdev.pl>
+
+#include <assert.h>
+#include <errno.h>
+#include <gpiod.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "internal.h"
+
+struct gpiod_request_config {
+       char consumer[GPIO_MAX_NAME_SIZE];
+       size_t event_buffer_size;
+};
+
+GPIOD_API struct gpiod_request_config *gpiod_request_config_new(void)
+{
+       struct gpiod_request_config *config;
+
+       config = malloc(sizeof(*config));
+       if (!config)
+               return NULL;
+
+       memset(config, 0, sizeof(*config));
+
+       return config;
+}
+
+GPIOD_API void gpiod_request_config_free(struct gpiod_request_config *config)
+{
+       free(config);
+}
+
+GPIOD_API void
+gpiod_request_config_set_consumer(struct gpiod_request_config *config,
+                                 const char *consumer)
+{
+       assert(config);
+
+       if (!consumer) {
+               config->consumer[0] = '\0';
+       } else {
+               strncpy(config->consumer, consumer, GPIO_MAX_NAME_SIZE - 1);
+               config->consumer[GPIO_MAX_NAME_SIZE - 1] = '\0';
+       }
+}
+
+GPIOD_API const char *
+gpiod_request_config_get_consumer(struct gpiod_request_config *config)
+{
+       assert(config);
+
+       return config->consumer[0] == '\0' ? NULL : config->consumer;
+}
+
+GPIOD_API void
+gpiod_request_config_set_event_buffer_size(struct gpiod_request_config *config,
+                                          size_t event_buffer_size)
+{
+       assert(config);
+
+       config->event_buffer_size = event_buffer_size;
+}
+
+GPIOD_API size_t
+gpiod_request_config_get_event_buffer_size(struct gpiod_request_config *config)
+{
+       assert(config);
+
+       return config->event_buffer_size;
+}
+
+void gpiod_request_config_to_uapi(struct gpiod_request_config *config,
+                                 struct gpio_v2_line_request *uapi_req)
+{
+       strcpy(uapi_req->consumer, config->consumer);
+       uapi_req->event_buffer_size = config->event_buffer_size;
+}
diff --git a/lib/uapi/gpio.h b/lib/uapi/gpio.h
new file mode 100644 (file)
index 0000000..cb9966d
--- /dev/null
@@ -0,0 +1,531 @@
+/* SPDX-License-Identifier: GPL-2.0-only WITH Linux-syscall-note */
+/*
+ * <linux/gpio.h> - userspace ABI for the GPIO character devices
+ *
+ * Copyright (C) 2016 Linus Walleij
+ *
+ * This program is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 as published by
+ * the Free Software Foundation.
+ */
+#ifndef _UAPI_GPIO_H_
+#define _UAPI_GPIO_H_
+
+#include <linux/const.h>
+#include <linux/ioctl.h>
+#include <linux/types.h>
+
+/*
+ * The maximum size of name and label arrays.
+ *
+ * Must be a multiple of 8 to ensure 32/64-bit alignment of structs.
+ */
+#define GPIO_MAX_NAME_SIZE 32
+
+/**
+ * struct gpiochip_info - Information about a certain GPIO chip
+ * @name: the Linux kernel name of this GPIO chip
+ * @label: a functional name for this GPIO chip, such as a product
+ * number, may be empty (i.e. label[0] == '\0')
+ * @lines: number of GPIO lines on this chip
+ */
+struct gpiochip_info {
+       char name[GPIO_MAX_NAME_SIZE];
+       char label[GPIO_MAX_NAME_SIZE];
+       __u32 lines;
+};
+
+/*
+ * Maximum number of requested lines.
+ *
+ * Must be no greater than 64, as bitmaps are restricted here to 64-bits
+ * for simplicity, and a multiple of 2 to ensure 32/64-bit alignment of
+ * structs.
+ */
+#define GPIO_V2_LINES_MAX 64
+
+/*
+ * The maximum number of configuration attributes associated with a line
+ * request.
+ */
+#define GPIO_V2_LINE_NUM_ATTRS_MAX 10
+
+/**
+ * enum gpio_v2_line_flag - &struct gpio_v2_line_attribute.flags values
+ * @GPIO_V2_LINE_FLAG_USED: line is not available for request
+ * @GPIO_V2_LINE_FLAG_ACTIVE_LOW: line active state is physical low
+ * @GPIO_V2_LINE_FLAG_INPUT: line is an input
+ * @GPIO_V2_LINE_FLAG_OUTPUT: line is an output
+ * @GPIO_V2_LINE_FLAG_EDGE_RISING: line detects rising (inactive to active)
+ * edges
+ * @GPIO_V2_LINE_FLAG_EDGE_FALLING: line detects falling (active to
+ * inactive) edges
+ * @GPIO_V2_LINE_FLAG_OPEN_DRAIN: line is an open drain output
+ * @GPIO_V2_LINE_FLAG_OPEN_SOURCE: line is an open source output
+ * @GPIO_V2_LINE_FLAG_BIAS_PULL_UP: line has pull-up bias enabled
+ * @GPIO_V2_LINE_FLAG_BIAS_PULL_DOWN: line has pull-down bias enabled
+ * @GPIO_V2_LINE_FLAG_BIAS_DISABLED: line has bias disabled
+ * @GPIO_V2_LINE_FLAG_EVENT_CLOCK_REALTIME: line events contain REALTIME timestamps
+ * @GPIO_V2_LINE_FLAG_EVENT_CLOCK_HTE: line events contain timestamps from
+ * hardware timestamp engine
+ */
+enum gpio_v2_line_flag {
+       GPIO_V2_LINE_FLAG_USED                  = _BITULL(0),
+       GPIO_V2_LINE_FLAG_ACTIVE_LOW            = _BITULL(1),
+       GPIO_V2_LINE_FLAG_INPUT                 = _BITULL(2),
+       GPIO_V2_LINE_FLAG_OUTPUT                = _BITULL(3),
+       GPIO_V2_LINE_FLAG_EDGE_RISING           = _BITULL(4),
+       GPIO_V2_LINE_FLAG_EDGE_FALLING          = _BITULL(5),
+       GPIO_V2_LINE_FLAG_OPEN_DRAIN            = _BITULL(6),
+       GPIO_V2_LINE_FLAG_OPEN_SOURCE           = _BITULL(7),
+       GPIO_V2_LINE_FLAG_BIAS_PULL_UP          = _BITULL(8),
+       GPIO_V2_LINE_FLAG_BIAS_PULL_DOWN        = _BITULL(9),
+       GPIO_V2_LINE_FLAG_BIAS_DISABLED         = _BITULL(10),
+       GPIO_V2_LINE_FLAG_EVENT_CLOCK_REALTIME  = _BITULL(11),
+       GPIO_V2_LINE_FLAG_EVENT_CLOCK_HTE       = _BITULL(12),
+};
+
+/**
+ * struct gpio_v2_line_values - Values of GPIO lines
+ * @bits: a bitmap containing the value of the lines, set to 1 for active
+ * and 0 for inactive.
+ * @mask: a bitmap identifying the lines to get or set, with each bit
+ * number corresponding to the index into &struct
+ * gpio_v2_line_request.offsets.
+ */
+struct gpio_v2_line_values {
+       __aligned_u64 bits;
+       __aligned_u64 mask;
+};
+
+/**
+ * enum gpio_v2_line_attr_id - &struct gpio_v2_line_attribute.id values
+ * identifying which field of the attribute union is in use.
+ * @GPIO_V2_LINE_ATTR_ID_FLAGS: flags field is in use
+ * @GPIO_V2_LINE_ATTR_ID_OUTPUT_VALUES: values field is in use
+ * @GPIO_V2_LINE_ATTR_ID_DEBOUNCE: debounce_period_us field is in use
+ */
+enum gpio_v2_line_attr_id {
+       GPIO_V2_LINE_ATTR_ID_FLAGS              = 1,
+       GPIO_V2_LINE_ATTR_ID_OUTPUT_VALUES      = 2,
+       GPIO_V2_LINE_ATTR_ID_DEBOUNCE           = 3,
+};
+
+/**
+ * struct gpio_v2_line_attribute - a configurable attribute of a line
+ * @id: attribute identifier with value from &enum gpio_v2_line_attr_id
+ * @padding: reserved for future use and must be zero filled
+ * @flags: if id is %GPIO_V2_LINE_ATTR_ID_FLAGS, the flags for the GPIO
+ * line, with values from &enum gpio_v2_line_flag, such as
+ * %GPIO_V2_LINE_FLAG_ACTIVE_LOW, %GPIO_V2_LINE_FLAG_OUTPUT etc, added
+ * together.  This overrides the default flags contained in the &struct
+ * gpio_v2_line_config for the associated line.
+ * @values: if id is %GPIO_V2_LINE_ATTR_ID_OUTPUT_VALUES, a bitmap
+ * containing the values to which the lines will be set, with each bit
+ * number corresponding to the index into &struct
+ * gpio_v2_line_request.offsets.
+ * @debounce_period_us: if id is %GPIO_V2_LINE_ATTR_ID_DEBOUNCE, the
+ * desired debounce period, in microseconds
+ */
+struct gpio_v2_line_attribute {
+       __u32 id;
+       __u32 padding;
+       union {
+               __aligned_u64 flags;
+               __aligned_u64 values;
+               __u32 debounce_period_us;
+       };
+};
+
+/**
+ * struct gpio_v2_line_config_attribute - a configuration attribute
+ * associated with one or more of the requested lines.
+ * @attr: the configurable attribute
+ * @mask: a bitmap identifying the lines to which the attribute applies,
+ * with each bit number corresponding to the index into &struct
+ * gpio_v2_line_request.offsets.
+ */
+struct gpio_v2_line_config_attribute {
+       struct gpio_v2_line_attribute attr;
+       __aligned_u64 mask;
+};
+
+/**
+ * struct gpio_v2_line_config - Configuration for GPIO lines
+ * @flags: flags for the GPIO lines, with values from &enum
+ * gpio_v2_line_flag, such as %GPIO_V2_LINE_FLAG_ACTIVE_LOW,
+ * %GPIO_V2_LINE_FLAG_OUTPUT etc, added together.  This is the default for
+ * all requested lines but may be overridden for particular lines using
+ * @attrs.
+ * @num_attrs: the number of attributes in @attrs
+ * @padding: reserved for future use and must be zero filled
+ * @attrs: the configuration attributes associated with the requested
+ * lines.  Any attribute should only be associated with a particular line
+ * once.  If an attribute is associated with a line multiple times then the
+ * first occurrence (i.e. lowest index) has precedence.
+ */
+struct gpio_v2_line_config {
+       __aligned_u64 flags;
+       __u32 num_attrs;
+       /* Pad to fill implicit padding and reserve space for future use. */
+       __u32 padding[5];
+       struct gpio_v2_line_config_attribute attrs[GPIO_V2_LINE_NUM_ATTRS_MAX];
+};
+
+/**
+ * struct gpio_v2_line_request - Information about a request for GPIO lines
+ * @offsets: an array of desired lines, specified by offset index for the
+ * associated GPIO chip
+ * @consumer: a desired consumer label for the selected GPIO lines such as
+ * "my-bitbanged-relay"
+ * @config: requested configuration for the lines.
+ * @num_lines: number of lines requested in this request, i.e. the number
+ * of valid fields in the %GPIO_V2_LINES_MAX sized arrays, set to 1 to
+ * request a single line
+ * @event_buffer_size: a suggested minimum number of line events that the
+ * kernel should buffer.  This is only relevant if edge detection is
+ * enabled in the configuration. Note that this is only a suggested value
+ * and the kernel may allocate a larger buffer or cap the size of the
+ * buffer. If this field is zero then the buffer size defaults to a minimum
+ * of @num_lines * 16.
+ * @padding: reserved for future use and must be zero filled
+ * @fd: if successful this field will contain a valid anonymous file handle
+ * after a %GPIO_GET_LINE_IOCTL operation, zero or negative value means
+ * error
+ */
+struct gpio_v2_line_request {
+       __u32 offsets[GPIO_V2_LINES_MAX];
+       char consumer[GPIO_MAX_NAME_SIZE];
+       struct gpio_v2_line_config config;
+       __u32 num_lines;
+       __u32 event_buffer_size;
+       /* Pad to fill implicit padding and reserve space for future use. */
+       __u32 padding[5];
+       __s32 fd;
+};
+
+/**
+ * struct gpio_v2_line_info - Information about a certain GPIO line
+ * @name: the name of this GPIO line, such as the output pin of the line on
+ * the chip, a rail or a pin header name on a board, as specified by the
+ * GPIO chip, may be empty (i.e. name[0] == '\0')
+ * @consumer: a functional name for the consumer of this GPIO line as set
+ * by whatever is using it, will be empty if there is no current user but
+ * may also be empty if the consumer doesn't set this up
+ * @offset: the local offset on this GPIO chip, fill this in when
+ * requesting the line information from the kernel
+ * @num_attrs: the number of attributes in @attrs
+ * @flags: flags for this GPIO line, with values from &enum
+ * gpio_v2_line_flag, such as %GPIO_V2_LINE_FLAG_ACTIVE_LOW,
+ * %GPIO_V2_LINE_FLAG_OUTPUT etc, added together.
+ * @attrs: the configuration attributes associated with the line
+ * @padding: reserved for future use
+ */
+struct gpio_v2_line_info {
+       char name[GPIO_MAX_NAME_SIZE];
+       char consumer[GPIO_MAX_NAME_SIZE];
+       __u32 offset;
+       __u32 num_attrs;
+       __aligned_u64 flags;
+       struct gpio_v2_line_attribute attrs[GPIO_V2_LINE_NUM_ATTRS_MAX];
+       /* Space reserved for future use. */
+       __u32 padding[4];
+};
+
+/**
+ * enum gpio_v2_line_changed_type - &struct gpio_v2_line_changed.event_type
+ * values
+ * @GPIO_V2_LINE_CHANGED_REQUESTED: line has been requested
+ * @GPIO_V2_LINE_CHANGED_RELEASED: line has been released
+ * @GPIO_V2_LINE_CHANGED_CONFIG: line has been reconfigured
+ */
+enum gpio_v2_line_changed_type {
+       GPIO_V2_LINE_CHANGED_REQUESTED  = 1,
+       GPIO_V2_LINE_CHANGED_RELEASED   = 2,
+       GPIO_V2_LINE_CHANGED_CONFIG     = 3,
+};
+
+/**
+ * struct gpio_v2_line_info_changed - Information about a change in status
+ * of a GPIO line
+ * @info: updated line information
+ * @timestamp_ns: estimate of time of status change occurrence, in nanoseconds
+ * @event_type: the type of change with a value from &enum
+ * gpio_v2_line_changed_type
+ * @padding: reserved for future use
+ */
+struct gpio_v2_line_info_changed {
+       struct gpio_v2_line_info info;
+       __aligned_u64 timestamp_ns;
+       __u32 event_type;
+       /* Pad struct to 64-bit boundary and reserve space for future use. */
+       __u32 padding[5];
+};
+
+/**
+ * enum gpio_v2_line_event_id - &struct gpio_v2_line_event.id values
+ * @GPIO_V2_LINE_EVENT_RISING_EDGE: event triggered by a rising edge
+ * @GPIO_V2_LINE_EVENT_FALLING_EDGE: event triggered by a falling edge
+ */
+enum gpio_v2_line_event_id {
+       GPIO_V2_LINE_EVENT_RISING_EDGE  = 1,
+       GPIO_V2_LINE_EVENT_FALLING_EDGE = 2,
+};
+
+/**
+ * struct gpio_v2_line_event - The actual event being pushed to userspace
+ * @timestamp_ns: best estimate of time of event occurrence, in nanoseconds.
+ * @id: event identifier with value from &enum gpio_v2_line_event_id
+ * @offset: the offset of the line that triggered the event
+ * @seqno: the sequence number for this event in the sequence of events for
+ * all the lines in this line request
+ * @line_seqno: the sequence number for this event in the sequence of
+ * events on this particular line
+ * @padding: reserved for future use
+ *
+ * By default the @timestamp_ns is read from %CLOCK_MONOTONIC and is
+ * intended to allow the accurate measurement of the time between events.
+ * It does not provide the wall-clock time.
+ *
+ * If the %GPIO_V2_LINE_FLAG_EVENT_CLOCK_REALTIME flag is set then the
+ * @timestamp_ns is read from %CLOCK_REALTIME.
+ */
+struct gpio_v2_line_event {
+       __aligned_u64 timestamp_ns;
+       __u32 id;
+       __u32 offset;
+       __u32 seqno;
+       __u32 line_seqno;
+       /* Space reserved for future use. */
+       __u32 padding[6];
+};
+
+/*
+ * ABI v1
+ *
+ * This version of the ABI is deprecated.
+ * Use the latest version of the ABI, defined above, instead.
+ */
+
+/* Informational flags */
+#define GPIOLINE_FLAG_KERNEL           (1UL << 0) /* Line used by the kernel */
+#define GPIOLINE_FLAG_IS_OUT           (1UL << 1)
+#define GPIOLINE_FLAG_ACTIVE_LOW       (1UL << 2)
+#define GPIOLINE_FLAG_OPEN_DRAIN       (1UL << 3)
+#define GPIOLINE_FLAG_OPEN_SOURCE      (1UL << 4)
+#define GPIOLINE_FLAG_BIAS_PULL_UP     (1UL << 5)
+#define GPIOLINE_FLAG_BIAS_PULL_DOWN   (1UL << 6)
+#define GPIOLINE_FLAG_BIAS_DISABLE     (1UL << 7)
+
+/**
+ * struct gpioline_info - Information about a certain GPIO line
+ * @line_offset: the local offset on this GPIO device, fill this in when
+ * requesting the line information from the kernel
+ * @flags: various flags for this line
+ * @name: the name of this GPIO line, such as the output pin of the line on the
+ * chip, a rail or a pin header name on a board, as specified by the gpio
+ * chip, may be empty (i.e. name[0] == '\0')
+ * @consumer: a functional name for the consumer of this GPIO line as set by
+ * whatever is using it, will be empty if there is no current user but may
+ * also be empty if the consumer doesn't set this up
+ *
+ * Note: This struct is part of ABI v1 and is deprecated.
+ * Use &struct gpio_v2_line_info instead.
+ */
+struct gpioline_info {
+       __u32 line_offset;
+       __u32 flags;
+       char name[GPIO_MAX_NAME_SIZE];
+       char consumer[GPIO_MAX_NAME_SIZE];
+};
+
+/* Maximum number of requested handles */
+#define GPIOHANDLES_MAX 64
+
+/* Possible line status change events */
+enum {
+       GPIOLINE_CHANGED_REQUESTED = 1,
+       GPIOLINE_CHANGED_RELEASED,
+       GPIOLINE_CHANGED_CONFIG,
+};
+
+/**
+ * struct gpioline_info_changed - Information about a change in status
+ * of a GPIO line
+ * @info: updated line information
+ * @timestamp: estimate of time of status change occurrence, in nanoseconds
+ * @event_type: one of %GPIOLINE_CHANGED_REQUESTED,
+ * %GPIOLINE_CHANGED_RELEASED and %GPIOLINE_CHANGED_CONFIG
+ * @padding: reserved for future use
+ *
+ * The &struct gpioline_info embedded here has 32-bit alignment on its own,
+ * but it works fine with 64-bit alignment too. With its 72 byte size, we can
+ * guarantee there are no implicit holes between it and subsequent members.
+ * The 20-byte padding at the end makes sure we don't add any implicit padding
+ * at the end of the structure on 64-bit architectures.
+ *
+ * Note: This struct is part of ABI v1 and is deprecated.
+ * Use &struct gpio_v2_line_info_changed instead.
+ */
+struct gpioline_info_changed {
+       struct gpioline_info info;
+       __u64 timestamp;
+       __u32 event_type;
+       __u32 padding[5]; /* for future use */
+};
+
+/* Linerequest flags */
+#define GPIOHANDLE_REQUEST_INPUT       (1UL << 0)
+#define GPIOHANDLE_REQUEST_OUTPUT      (1UL << 1)
+#define GPIOHANDLE_REQUEST_ACTIVE_LOW  (1UL << 2)
+#define GPIOHANDLE_REQUEST_OPEN_DRAIN  (1UL << 3)
+#define GPIOHANDLE_REQUEST_OPEN_SOURCE (1UL << 4)
+#define GPIOHANDLE_REQUEST_BIAS_PULL_UP        (1UL << 5)
+#define GPIOHANDLE_REQUEST_BIAS_PULL_DOWN      (1UL << 6)
+#define GPIOHANDLE_REQUEST_BIAS_DISABLE        (1UL << 7)
+
+/**
+ * struct gpiohandle_request - Information about a GPIO handle request
+ * @lineoffsets: an array of desired lines, specified by offset index for the
+ * associated GPIO device
+ * @flags: desired flags for the desired GPIO lines, such as
+ * %GPIOHANDLE_REQUEST_OUTPUT, %GPIOHANDLE_REQUEST_ACTIVE_LOW etc, added
+ * together. Note that even if multiple lines are requested, the same flags
+ * must be applicable to all of them, if you want lines with individual
+ * flags set, request them one by one. It is possible to select
+ * a batch of input or output lines, but they must all have the same
+ * characteristics, i.e. all inputs or all outputs, all active low etc
+ * @default_values: if the %GPIOHANDLE_REQUEST_OUTPUT is set for a requested
+ * line, this specifies the default output value, should be 0 (low) or
+ * 1 (high), anything else than 0 or 1 will be interpreted as 1 (high)
+ * @consumer_label: a desired consumer label for the selected GPIO line(s)
+ * such as "my-bitbanged-relay"
+ * @lines: number of lines requested in this request, i.e. the number of
+ * valid fields in the above arrays, set to 1 to request a single line
+ * @fd: if successful this field will contain a valid anonymous file handle
+ * after a %GPIO_GET_LINEHANDLE_IOCTL operation, zero or negative value
+ * means error
+ *
+ * Note: This struct is part of ABI v1 and is deprecated.
+ * Use &struct gpio_v2_line_request instead.
+ */
+struct gpiohandle_request {
+       __u32 lineoffsets[GPIOHANDLES_MAX];
+       __u32 flags;
+       __u8 default_values[GPIOHANDLES_MAX];
+       char consumer_label[GPIO_MAX_NAME_SIZE];
+       __u32 lines;
+       int fd;
+};
+
+/**
+ * struct gpiohandle_config - Configuration for a GPIO handle request
+ * @flags: updated flags for the requested GPIO lines, such as
+ * %GPIOHANDLE_REQUEST_OUTPUT, %GPIOHANDLE_REQUEST_ACTIVE_LOW etc, added
+ * together
+ * @default_values: if the %GPIOHANDLE_REQUEST_OUTPUT is set in flags,
+ * this specifies the default output value, should be 0 (low) or
+ * 1 (high), anything else than 0 or 1 will be interpreted as 1 (high)
+ * @padding: reserved for future use and should be zero filled
+ *
+ * Note: This struct is part of ABI v1 and is deprecated.
+ * Use &struct gpio_v2_line_config instead.
+ */
+struct gpiohandle_config {
+       __u32 flags;
+       __u8 default_values[GPIOHANDLES_MAX];
+       __u32 padding[4]; /* padding for future use */
+};
+
+/**
+ * struct gpiohandle_data - Information of values on a GPIO handle
+ * @values: when getting the state of lines this contains the current
+ * state of a line, when setting the state of lines these should contain
+ * the desired target state
+ *
+ * Note: This struct is part of ABI v1 and is deprecated.
+ * Use &struct gpio_v2_line_values instead.
+ */
+struct gpiohandle_data {
+       __u8 values[GPIOHANDLES_MAX];
+};
+
+/* Eventrequest flags */
+#define GPIOEVENT_REQUEST_RISING_EDGE  (1UL << 0)
+#define GPIOEVENT_REQUEST_FALLING_EDGE (1UL << 1)
+#define GPIOEVENT_REQUEST_BOTH_EDGES   ((1UL << 0) | (1UL << 1))
+
+/**
+ * struct gpioevent_request - Information about a GPIO event request
+ * @lineoffset: the desired line to subscribe to events from, specified by
+ * offset index for the associated GPIO device
+ * @handleflags: desired handle flags for the desired GPIO line, such as
+ * %GPIOHANDLE_REQUEST_ACTIVE_LOW or %GPIOHANDLE_REQUEST_OPEN_DRAIN
+ * @eventflags: desired flags for the desired GPIO event line, such as
+ * %GPIOEVENT_REQUEST_RISING_EDGE or %GPIOEVENT_REQUEST_FALLING_EDGE
+ * @consumer_label: a desired consumer label for the selected GPIO line(s)
+ * such as "my-listener"
+ * @fd: if successful this field will contain a valid anonymous file handle
+ * after a %GPIO_GET_LINEEVENT_IOCTL operation, zero or negative value
+ * means error
+ *
+ * Note: This struct is part of ABI v1 and is deprecated.
+ * Use &struct gpio_v2_line_request instead.
+ */
+struct gpioevent_request {
+       __u32 lineoffset;
+       __u32 handleflags;
+       __u32 eventflags;
+       char consumer_label[GPIO_MAX_NAME_SIZE];
+       int fd;
+};
+
+/*
+ * GPIO event types
+ */
+#define GPIOEVENT_EVENT_RISING_EDGE 0x01
+#define GPIOEVENT_EVENT_FALLING_EDGE 0x02
+
+/**
+ * struct gpioevent_data - The actual event being pushed to userspace
+ * @timestamp: best estimate of time of event occurrence, in nanoseconds
+ * @id: event identifier
+ *
+ * Note: This struct is part of ABI v1 and is deprecated.
+ * Use &struct gpio_v2_line_event instead.
+ */
+struct gpioevent_data {
+       __u64 timestamp;
+       __u32 id;
+};
+
+/*
+ * v1 and v2 ioctl()s
+ */
+#define GPIO_GET_CHIPINFO_IOCTL _IOR(0xB4, 0x01, struct gpiochip_info)
+#define GPIO_GET_LINEINFO_UNWATCH_IOCTL _IOWR(0xB4, 0x0C, __u32)
+
+/*
+ * v2 ioctl()s
+ */
+#define GPIO_V2_GET_LINEINFO_IOCTL _IOWR(0xB4, 0x05, struct gpio_v2_line_info)
+#define GPIO_V2_GET_LINEINFO_WATCH_IOCTL _IOWR(0xB4, 0x06, struct gpio_v2_line_info)
+#define GPIO_V2_GET_LINE_IOCTL _IOWR(0xB4, 0x07, struct gpio_v2_line_request)
+#define GPIO_V2_LINE_SET_CONFIG_IOCTL _IOWR(0xB4, 0x0D, struct gpio_v2_line_config)
+#define GPIO_V2_LINE_GET_VALUES_IOCTL _IOWR(0xB4, 0x0E, struct gpio_v2_line_values)
+#define GPIO_V2_LINE_SET_VALUES_IOCTL _IOWR(0xB4, 0x0F, struct gpio_v2_line_values)
+
+/*
+ * v1 ioctl()s
+ *
+ * These ioctl()s are deprecated.  Use the v2 equivalent instead.
+ */
+#define GPIO_GET_LINEINFO_IOCTL _IOWR(0xB4, 0x02, struct gpioline_info)
+#define GPIO_GET_LINEHANDLE_IOCTL _IOWR(0xB4, 0x03, struct gpiohandle_request)
+#define GPIO_GET_LINEEVENT_IOCTL _IOWR(0xB4, 0x04, struct gpioevent_request)
+#define GPIOHANDLE_GET_LINE_VALUES_IOCTL _IOWR(0xB4, 0x08, struct gpiohandle_data)
+#define GPIOHANDLE_SET_LINE_VALUES_IOCTL _IOWR(0xB4, 0x09, struct gpiohandle_data)
+#define GPIOHANDLE_SET_CONFIG_IOCTL _IOWR(0xB4, 0x0A, struct gpiohandle_config)
+#define GPIO_GET_LINEINFO_WATCH_IOCTL _IOWR(0xB4, 0x0B, struct gpioline_info)
+
+#endif /* _UAPI_GPIO_H_ */
diff --git a/man/.gitignore b/man/.gitignore
new file mode 100644 (file)
index 0000000..1263cf7
--- /dev/null
@@ -0,0 +1,4 @@
+# SPDX-License-Identifier: GPL-2.0-or-later
+# SPDX-FileCopyrightText: 2017-2021 Bartosz Golaszewski <bartekgola@gmail.com>
+
+*.man
diff --git a/man/Makefile.am b/man/Makefile.am
new file mode 100644 (file)
index 0000000..ddd0628
--- /dev/null
@@ -0,0 +1,22 @@
+# SPDX-License-Identifier: GPL-2.0-or-later
+# SPDX-FileCopyrightText: 2017-2021 Bartosz Golaszewski <bartekgola@gmail.com>
+
+if WITH_MANPAGES
+
+dist_man1_MANS = \
+       gpiodetect.man \
+       gpioinfo.man \
+       gpioget.man \
+       gpioset.man \
+       gpiomon.man \
+       gpionotify.man
+
+%.man: $(top_builddir)/tools/$(*F)
+       $(AM_V_GEN)help2man $(top_builddir)/tools/$(*F) --include=$(srcdir)/template --output=$(builddir)/$@ --no-info
+
+clean-local:
+       rm -f $(dist_man1_MANS)
+
+endif
+
+EXTRA_DIST = template
diff --git a/man/template b/man/template
new file mode 100644 (file)
index 0000000..0602b6b
--- /dev/null
@@ -0,0 +1,10 @@
+# SPDX-License-Identifier: CC-BY-SA-4.0
+# SPDX-FileCopyrightText: 2017-2021 Bartosz Golaszewski <bartekgola@gmail.com>
+
+[AUTHOR]
+Bartosz Golaszewski <brgl@bgdev.pl>
+
+[REPORTING BUGS]
+Report bugs to:
+    Bartosz Golaszewski <brgl@bgdev.pl>
+    linux-gpio <linux-gpio@vger.kernel.org>
diff --git a/sphinx/conf.py b/sphinx/conf.py
new file mode 100644 (file)
index 0000000..51ae3e9
--- /dev/null
@@ -0,0 +1,66 @@
+# SPDX-License-Identifier: LGPL-2.1-or-later
+# SPDX-FileCopyrightText: 2022 Kent Gibson <warthog618@gmail.com>
+
+# This file is part of libgpiod.
+#
+# Configuration file for the Sphinx documentation builder.
+#
+# This file only contains a selection of the most common options. For a full
+# list see the documentation:
+# https://www.sphinx-doc.org/en/master/usage/configuration.html
+
+import subprocess
+
+subprocess.run("cd .. ; ./autogen.sh ; make doc", shell=True)
+
+# -- Path setup --------------------------------------------------------------
+
+# If extensions (or modules to document with autodoc) are in another directory,
+# add these directories to sys.path here. If the directory is relative to the
+# documentation root, use os.path.abspath to make it absolute, like shown here.
+#
+# import os
+# import sys
+# sys.path.insert(0, os.path.abspath('.'))
+
+# -- Project information -----------------------------------------------------
+
+project = "libgpiod"
+copyright = "2022, Bartosz Golaszewski"
+author = "Bartosz Golaszewski"
+
+# The full version, including alpha/beta/rc tags
+release = (
+    subprocess.run(["git", "describe", "--dirty"], capture_output=True)
+    .stdout.decode()
+    .strip()
+)
+
+# -- General configuration ---------------------------------------------------
+
+# Add any Sphinx extension module names here, as strings. They can be
+# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
+# ones.
+extensions = []
+
+# Add any paths that contain templates here, relative to this directory.
+templates_path = []
+
+# List of patterns, relative to source directory, that match files and
+# directories to ignore when looking for source files.
+# This pattern also affects html_static_path and html_extra_path.
+exclude_patterns = []
+
+# -- Options for HTML output -------------------------------------------------
+
+# The theme to use for HTML and HTML Help pages.  See the documentation for
+# a list of builtin themes.
+#
+html_theme = "alabaster"
+
+# Add any paths that contain custom static files (such as style sheets) here,
+# relative to this directory. They are copied after the builtin static files,
+# so a file named "default.css" will overwrite the builtin "default.css".
+html_static_path = []
+
+html_extra_path = ["../doc/html"]
diff --git a/sphinx/index.rst b/sphinx/index.rst
new file mode 100644 (file)
index 0000000..c26d068
--- /dev/null
@@ -0,0 +1,24 @@
+..
+   SPDX-License-Identifier: LGPL-2.1-or-later
+   SPDX-FileCopyrightText: 2022 Kent Gibson <warthog618@gmail.com>
+
+..
+   This file is part of libgpiod.
+
+   libgpiod documentation master file.
+
+Welcome to libgpiod's documentation!
+====================================
+
+.. toctree::
+   :maxdepth: 2
+   :caption: Contents:
+
+
+
+Indices and tables
+==================
+
+* :ref:`genindex`
+* :ref:`modindex`
+* :ref:`search`
diff --git a/tests/.gitignore b/tests/.gitignore
new file mode 100644 (file)
index 0000000..676f092
--- /dev/null
@@ -0,0 +1,4 @@
+# SPDX-License-Identifier: GPL-2.0-or-later
+# SPDX-FileCopyrightText: 2017-2021 Bartosz Golaszewski <bartekgola@gmail.com>
+
+gpiod-test
diff --git a/tests/Makefile.am b/tests/Makefile.am
new file mode 100644 (file)
index 0000000..0680d5e
--- /dev/null
@@ -0,0 +1,32 @@
+# SPDX-License-Identifier: GPL-2.0-or-later
+# SPDX-FileCopyrightText: 2017-2022 Bartosz Golaszewski <brgl@bgdev.pl>
+
+SUBDIRS = gpiosim
+
+AM_CFLAGS = -I$(top_srcdir)/include/ -I$(top_srcdir)/tests/gpiosim/
+AM_CFLAGS += -include $(top_builddir)/config.h
+AM_CFLAGS += -Wall -Wextra -g -std=gnu89 $(GLIB_CFLAGS) $(GIO_CFLAGS)
+AM_CFLAGS += -DG_LOG_DOMAIN=\"gpiod-test\"
+LDADD = $(top_builddir)/lib/libgpiod.la
+LDADD += $(top_builddir)/tests/gpiosim/libgpiosim.la
+LDADD += $(GLIB_LIBS) $(GIO_LIBS)
+
+noinst_PROGRAMS = gpiod-test
+
+gpiod_test_SOURCES = \
+       gpiod-test.c \
+       gpiod-test.h \
+       gpiod-test-helpers.c \
+       gpiod-test-helpers.h \
+       gpiod-test-sim.c \
+       gpiod-test-sim.h \
+       tests-chip.c \
+       tests-chip-info.c \
+       tests-edge-event.c \
+       tests-info-event.c \
+       tests-line-config.c \
+       tests-line-info.c \
+       tests-line-request.c \
+       tests-line-settings.c \
+       tests-misc.c \
+       tests-request-config.c
diff --git a/tests/gpiod-test-helpers.c b/tests/gpiod-test-helpers.c
new file mode 100644 (file)
index 0000000..7e5b396
--- /dev/null
@@ -0,0 +1,41 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/* SPDX-FileCopyrightText: 2017-2022 Bartosz Golaszewski <brgl@bgdev.pl> */
+
+/*
+ * Testing framework for the core library.
+ *
+ * This file contains functions and definitions extending the GLib unit testing
+ * framework with functionalities necessary to test the libgpiod core C API as
+ * well as the kernel-to-user-space interface.
+ */
+
+#include "gpiod-test-helpers.h"
+
+GVariant *
+gpiod_test_package_line_names(const GPIOSimLineName *names)
+{
+       g_autoptr(GVariantBuilder) builder = NULL;
+       const GPIOSimLineName *name;
+
+       builder = g_variant_builder_new(G_VARIANT_TYPE("a(us)"));
+
+       for (name = &names[0]; name->name; name++)
+               g_variant_builder_add(builder, "(us)",
+                                     name->offset, name->name);
+
+       return g_variant_ref_sink(g_variant_new("a(us)", builder));
+}
+
+GVariant *gpiod_test_package_hogs(const GPIOSimHog *hogs)
+{
+       g_autoptr(GVariantBuilder) builder = NULL;
+       const GPIOSimHog *hog;
+
+       builder = g_variant_builder_new(G_VARIANT_TYPE("a(usi)"));
+
+       for (hog = &hogs[0]; hog->name; hog++)
+               g_variant_builder_add(builder, "(usi)",
+                                     hog->offset, hog->name, hog->direction);
+
+       return g_variant_ref_sink(g_variant_new("a(usi)", builder));
+}
diff --git a/tests/gpiod-test-helpers.h b/tests/gpiod-test-helpers.h
new file mode 100644 (file)
index 0000000..41791a3
--- /dev/null
@@ -0,0 +1,203 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/* SPDX-FileCopyrightText: 2022 Bartosz Golaszewski <brgl@bgdev.pl> */
+
+#ifndef __GPIOD_TEST_HELPERS_H__
+#define __GPIOD_TEST_HELPERS_H__
+
+#include <errno.h>
+#include <glib.h>
+#include <gpiod.h>
+
+#include "gpiod-test-sim.h"
+
+/*
+ * These typedefs are needed to make g_autoptr work - it doesn't accept
+ * regular 'struct typename' syntax.
+ */
+
+typedef struct gpiod_chip struct_gpiod_chip;
+G_DEFINE_AUTOPTR_CLEANUP_FUNC(struct_gpiod_chip, gpiod_chip_close);
+
+typedef struct gpiod_chip_info struct_gpiod_chip_info;
+G_DEFINE_AUTOPTR_CLEANUP_FUNC(struct_gpiod_chip_info, gpiod_chip_info_free);
+
+typedef struct gpiod_line_info struct_gpiod_line_info;
+G_DEFINE_AUTOPTR_CLEANUP_FUNC(struct_gpiod_line_info, gpiod_line_info_free);
+
+typedef struct gpiod_info_event struct_gpiod_info_event;
+G_DEFINE_AUTOPTR_CLEANUP_FUNC(struct_gpiod_info_event, gpiod_info_event_free);
+
+typedef struct gpiod_line_config struct_gpiod_line_config;
+G_DEFINE_AUTOPTR_CLEANUP_FUNC(struct_gpiod_line_config, gpiod_line_config_free);
+
+typedef struct gpiod_line_settings struct_gpiod_line_settings;
+G_DEFINE_AUTOPTR_CLEANUP_FUNC(struct_gpiod_line_settings,
+                             gpiod_line_settings_free);
+
+typedef struct gpiod_request_config struct_gpiod_request_config;
+G_DEFINE_AUTOPTR_CLEANUP_FUNC(struct_gpiod_request_config,
+                             gpiod_request_config_free);
+
+typedef struct gpiod_line_request struct_gpiod_line_request;
+G_DEFINE_AUTOPTR_CLEANUP_FUNC(struct_gpiod_line_request,
+                             gpiod_line_request_release);
+
+typedef struct gpiod_edge_event struct_gpiod_edge_event;
+G_DEFINE_AUTOPTR_CLEANUP_FUNC(struct_gpiod_edge_event, gpiod_edge_event_free);
+
+typedef struct gpiod_edge_event_buffer struct_gpiod_edge_event_buffer;
+G_DEFINE_AUTOPTR_CLEANUP_FUNC(struct_gpiod_edge_event_buffer,
+                             gpiod_edge_event_buffer_free);
+
+#define gpiod_test_return_if_failed() \
+       do { \
+               if (g_test_failed()) \
+                       return; \
+       } while (0)
+
+#define gpiod_test_join_thread_and_return_if_failed(_thread) \
+       do { \
+               if (g_test_failed()) { \
+                       g_thread_join(_thread); \
+                       return; \
+               } \
+       } while (0)
+
+#define gpiod_test_open_chip_or_fail(_path) \
+       ({ \
+               struct gpiod_chip *_chip = gpiod_chip_open(_path); \
+               g_assert_nonnull(_chip); \
+               gpiod_test_return_if_failed(); \
+               _chip; \
+       })
+
+#define gpiod_test_chip_get_info_or_fail(_chip) \
+       ({ \
+               struct gpiod_chip_info *_info = gpiod_chip_get_info(_chip); \
+               g_assert_nonnull(_info); \
+               gpiod_test_return_if_failed(); \
+               _info; \
+       })
+
+#define gpiod_test_chip_get_line_info_or_fail(_chip, _offset) \
+       ({ \
+               struct gpiod_line_info *_info = \
+                               gpiod_chip_get_line_info(_chip, _offset); \
+               g_assert_nonnull(_info); \
+               gpiod_test_return_if_failed(); \
+               _info; \
+       })
+
+#define gpiod_test_chip_watch_line_info_or_fail(_chip, _offset) \
+       ({ \
+               struct gpiod_line_info *_info = \
+                               gpiod_chip_watch_line_info(_chip, _offset); \
+               g_assert_nonnull(_info); \
+               gpiod_test_return_if_failed(); \
+               _info; \
+       })
+
+#define gpiod_test_create_line_settings_or_fail() \
+       ({ \
+               struct gpiod_line_settings *_settings = \
+                               gpiod_line_settings_new(); \
+               g_assert_nonnull(_settings); \
+               gpiod_test_return_if_failed(); \
+               _settings; \
+       })
+
+#define gpiod_test_create_line_config_or_fail() \
+       ({ \
+               struct gpiod_line_config *_config = \
+                               gpiod_line_config_new(); \
+               g_assert_nonnull(_config); \
+               gpiod_test_return_if_failed(); \
+               _config; \
+       })
+
+#define gpiod_test_create_edge_event_buffer_or_fail(_capacity) \
+       ({ \
+               struct gpiod_edge_event_buffer *_buffer = \
+                               gpiod_edge_event_buffer_new(_capacity); \
+               g_assert_nonnull(_buffer); \
+               gpiod_test_return_if_failed(); \
+               _buffer; \
+       })
+
+#define gpiod_test_line_config_add_line_settings_or_fail(_line_cfg, _offsets, \
+                                                        _num_offsets, \
+                                                        _settings) \
+       do { \
+               gint _ret = gpiod_line_config_add_line_settings(_line_cfg, \
+                                                               _offsets,  \
+                                                               _num_offsets, \
+                                                               _settings); \
+               g_assert_cmpint(_ret, ==, 0); \
+               gpiod_test_return_if_failed(); \
+       } while (0)
+
+#define gpiod_test_line_config_get_line_settings_or_fail(_line_cfg, _offset) \
+       ({ \
+               struct gpiod_line_settings *_settings = \
+                       gpiod_line_config_get_line_settings(_line_cfg, \
+                                                           _offset); \
+               g_assert_nonnull(_settings); \
+               gpiod_test_return_if_failed(); \
+               _settings; \
+       })
+
+#define gpiod_test_line_config_set_output_values_or_fail(_line_cfg, _values, \
+                                                        _num_values) \
+       do { \
+               gint _ret = gpiod_line_config_set_output_values(_line_cfg, \
+                                                               _values, \
+                                                               _num_values); \
+               g_assert_cmpint(_ret, ==, 0); \
+               gpiod_test_return_if_failed(); \
+       } while (0)
+
+#define gpiod_test_create_request_config_or_fail() \
+       ({ \
+               struct gpiod_request_config *_config = \
+                               gpiod_request_config_new(); \
+               g_assert_nonnull(_config); \
+               gpiod_test_return_if_failed(); \
+               _config; \
+       })
+
+#define gpiod_test_chip_request_lines_or_fail(_chip, _req_cfg, _line_cfg) \
+       ({ \
+               struct gpiod_line_request *_request = \
+                       gpiod_chip_request_lines(_chip, \
+                                                _req_cfg, _line_cfg); \
+               g_assert_nonnull(_request); \
+               gpiod_test_return_if_failed(); \
+               _request; \
+       })
+
+#define gpiod_test_line_request_reconfigure_lines_or_fail(_request, _line_cfg) \
+       do { \
+               gint _ret = gpiod_line_request_reconfigure_lines(_request, \
+                                                                _line_cfg); \
+               g_assert_cmpint(_ret, ==, 0); \
+               gpiod_test_return_if_failed(); \
+       } while (0)
+
+#define gpiod_test_expect_errno(_expected) \
+       g_assert_cmpint(_expected, ==, errno)
+
+typedef struct {
+       guint offset;
+       const gchar *name;
+} GPIOSimLineName;
+
+typedef struct {
+       guint offset;
+       const gchar *name;
+       GPIOSimDirection direction;
+} GPIOSimHog;
+
+GVariant *gpiod_test_package_line_names(const GPIOSimLineName *names);
+GVariant *gpiod_test_package_hogs(const GPIOSimHog *hogs);
+
+#endif /* __GPIOD_TEST_HELPERS_H__ */
diff --git a/tests/gpiod-test-sim.c b/tests/gpiod-test-sim.c
new file mode 100644 (file)
index 0000000..ac6c71a
--- /dev/null
@@ -0,0 +1,464 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/* SPDX-FileCopyrightText: 2022 Bartosz Golaszewski <brgl@bgdev.pl> */
+
+#include <errno.h>
+#include <gpiosim.h>
+#include <stdlib.h>
+#include <unistd.h>
+
+#include "gpiod-test-sim.h"
+
+G_DEFINE_QUARK(g-gpiosim-error, g_gpiosim_error);
+
+struct _GPIOSimChip {
+       GObject parent_instance;
+       struct gpiosim_bank *bank;
+       GError *construct_err;
+       guint num_lines;
+       gchar *label;
+       GVariant *line_names;
+       GVariant *hogs;
+};
+
+enum {
+       G_GPIOSIM_CHIP_PROP_DEV_PATH = 1,
+       G_GPIOSIM_CHIP_PROP_NAME,
+       G_GPIOSIM_CHIP_PROP_NUM_LINES,
+       G_GPIOSIM_CHIP_PROP_LABEL,
+       G_GPIOSIM_CHIP_PROP_LINE_NAMES,
+       G_GPIOSIM_CHIP_PROP_HOGS,
+};
+
+static struct gpiosim_ctx *sim_ctx;
+
+static gboolean
+g_gpiosim_chip_initable_init(GInitable *initable,
+                            GCancellable *cancellable G_GNUC_UNUSED,
+                            GError **err)
+{
+       GPIOSimChip *self = G_GPIOSIM_CHIP_OBJ(initable);
+
+       if (self->construct_err) {
+               g_propagate_error(err, self->construct_err);
+               self->construct_err = NULL;
+               return FALSE;
+       }
+
+       return TRUE;
+}
+
+static void g_gpiosim_chip_initable_iface_init(GInitableIface *iface)
+{
+       iface->init = g_gpiosim_chip_initable_init;
+}
+
+G_DEFINE_TYPE_WITH_CODE(GPIOSimChip, g_gpiosim_chip, G_TYPE_OBJECT,
+                       G_IMPLEMENT_INTERFACE(
+                               G_TYPE_INITABLE,
+                               g_gpiosim_chip_initable_iface_init));
+
+static void g_gpiosim_ctx_unref(void)
+{
+       gpiosim_ctx_unref(sim_ctx);
+}
+
+static gboolean g_gpiosim_chip_apply_line_names(GPIOSimChip *self)
+{
+       g_autoptr(GVariantIter) iter = NULL;
+       guint offset;
+       gchar *name;
+       int ret;
+
+       if (!self->line_names)
+               return TRUE;
+
+       iter = g_variant_iter_new(self->line_names);
+
+       while (g_variant_iter_loop(iter, "(us)", &offset, &name)) {
+               ret = gpiosim_bank_set_line_name(self->bank, offset, name);
+               if (ret) {
+                       g_set_error(&self->construct_err, G_GPIOSIM_ERROR,
+                                   G_GPIOSIM_ERR_CHIP_INIT_FAILED,
+                                   "Unable to set the name of the simulated GPIO line: %s",
+                                   g_strerror(errno));
+                       return FALSE;
+               }
+       }
+
+       return TRUE;
+}
+
+static gboolean g_gpiosim_chip_apply_hogs(GPIOSimChip *self)
+{
+       g_autoptr(GVariantIter) iter = NULL;
+       enum gpiosim_direction dir;
+       guint offset;
+       gchar *name;
+       gint vdir;
+       int ret;
+
+       if (!self->hogs)
+               return TRUE;
+
+       iter = g_variant_iter_new(self->hogs);
+
+       while (g_variant_iter_loop(iter, "(usi)", &offset, &name, &vdir)) {
+               switch (vdir) {
+               case G_GPIOSIM_DIRECTION_INPUT:
+                       dir = GPIOSIM_DIRECTION_INPUT;
+                       break;
+               case G_GPIOSIM_DIRECTION_OUTPUT_HIGH:
+                       dir = GPIOSIM_DIRECTION_OUTPUT_HIGH;
+                       break;
+               case G_GPIOSIM_DIRECTION_OUTPUT_LOW:
+                       dir = GPIOSIM_DIRECTION_OUTPUT_LOW;
+                       break;
+               default:
+                       g_error("Invalid hog direction value: %d", vdir);
+               }
+
+               ret = gpiosim_bank_hog_line(self->bank, offset, name, dir);
+               if (ret) {
+                       g_set_error(&self->construct_err, G_GPIOSIM_ERROR,
+                                   G_GPIOSIM_ERR_CHIP_INIT_FAILED,
+                                   "Unable to hog the simulated GPIO line: %s",
+                                   g_strerror(errno));
+                       return FALSE;
+               }
+       }
+
+       return TRUE;
+}
+
+static gboolean g_gpiosim_chip_apply_properties(GPIOSimChip *self)
+{
+       int ret;
+
+       ret = gpiosim_bank_set_num_lines(self->bank, self->num_lines);
+       if (ret) {
+               g_set_error(&self->construct_err, G_GPIOSIM_ERROR,
+                           G_GPIOSIM_ERR_CHIP_INIT_FAILED,
+                           "Unable to set the number of lines exposed by the simulated chip: %s",
+                           g_strerror(errno));
+               return FALSE;
+       }
+
+       if (self->label) {
+               ret = gpiosim_bank_set_label(self->bank, self->label);
+               if (ret) {
+                       g_set_error(&self->construct_err, G_GPIOSIM_ERROR,
+                                   G_GPIOSIM_ERR_CHIP_INIT_FAILED,
+                                   "Unable to set the label of the simulated chip: %s",
+                                   g_strerror(errno));
+                       return FALSE;
+               }
+       }
+
+       ret = g_gpiosim_chip_apply_line_names(self);
+       if (!ret)
+               return FALSE;
+
+       return g_gpiosim_chip_apply_hogs(self);
+}
+
+static gboolean g_gpiosim_chip_enable(GPIOSimChip *self)
+{
+       struct gpiosim_dev *dev;
+       int ret;
+
+       dev = gpiosim_bank_get_dev(self->bank);
+       ret = gpiosim_dev_enable(dev);
+       gpiosim_dev_unref(dev);
+       if (ret) {
+               g_set_error(&self->construct_err, G_GPIOSIM_ERROR,
+                           G_GPIOSIM_ERR_CHIP_ENABLE_FAILED,
+                           "Error while trying to enable the simulated GPIO device: %s",
+                           g_strerror(errno));
+               return FALSE;
+       }
+
+       return TRUE;
+}
+
+static gboolean g_gpiosim_ctx_init(GError **err)
+{
+       sim_ctx = gpiosim_ctx_new();
+       if (!sim_ctx) {
+               g_set_error(err, G_GPIOSIM_ERROR,
+                           G_GPIOSIM_ERR_CTX_INIT_FAILED,
+                           "Unable to initialize libgpiosim: %s",
+                           g_strerror(errno));
+               return FALSE;
+       }
+
+       atexit(g_gpiosim_ctx_unref);
+
+       return TRUE;
+}
+
+static void g_gpiosim_chip_constructed(GObject *obj)
+{
+       GPIOSimChip *self = G_GPIOSIM_CHIP(obj);
+       struct gpiosim_dev *dev;
+       gboolean ret;
+
+       if (!sim_ctx) {
+               ret = g_gpiosim_ctx_init(&self->construct_err);
+               if (!ret)
+                       return;
+       }
+
+       dev = gpiosim_dev_new(sim_ctx);
+       if (!dev) {
+               g_set_error(&self->construct_err, G_GPIOSIM_ERROR,
+                           G_GPIOSIM_ERR_CHIP_INIT_FAILED,
+                           "Unable to instantiate new GPIO device: %s",
+                           g_strerror(errno));
+               return;
+       }
+
+       self->bank = gpiosim_bank_new(dev);
+       gpiosim_dev_unref(dev);
+       if (!self->bank) {
+               g_set_error(&self->construct_err, G_GPIOSIM_ERROR,
+                           G_GPIOSIM_ERR_CHIP_INIT_FAILED,
+                           "Unable to instantiate new GPIO bank: %s",
+                           g_strerror(errno));
+               return;
+       }
+
+       ret = g_gpiosim_chip_apply_properties(self);
+       if (!ret)
+               return;
+
+       ret = g_gpiosim_chip_enable(self);
+       if (!ret)
+               return;
+
+       G_OBJECT_CLASS(g_gpiosim_chip_parent_class)->constructed(obj);
+}
+
+static void g_gpiosim_chip_get_property(GObject *obj, guint prop_id,
+                                       GValue *val, GParamSpec *pspec)
+{
+       GPIOSimChip *self = G_GPIOSIM_CHIP(obj);
+
+       switch (prop_id) {
+       case G_GPIOSIM_CHIP_PROP_DEV_PATH:
+               g_value_set_static_string(val,
+                               gpiosim_bank_get_dev_path(self->bank));
+               break;
+       case G_GPIOSIM_CHIP_PROP_NAME:
+               g_value_set_static_string(val,
+                               gpiosim_bank_get_chip_name(self->bank));
+               break;
+       default:
+               G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, prop_id, pspec);
+               break;
+       }
+}
+
+static void set_variant_prop(GVariant **prop, const GValue *val)
+{
+       GVariant *variant = g_value_get_variant(val);
+
+       g_clear_pointer(prop, g_variant_unref);
+       if (variant)
+               *prop = g_variant_ref(variant);
+}
+
+static void g_gpiosim_chip_set_property(GObject *obj, guint prop_id,
+                                       const GValue *val, GParamSpec *pspec)
+{
+       GPIOSimChip *self = G_GPIOSIM_CHIP(obj);
+       const gchar *label;
+
+       switch (prop_id) {
+       case G_GPIOSIM_CHIP_PROP_NUM_LINES:
+               self->num_lines = g_value_get_uint(val);
+               break;
+       case G_GPIOSIM_CHIP_PROP_LABEL:
+               g_clear_pointer(&self->label, g_free);
+               label = g_value_get_string(val);
+               if (label)
+                       self->label = g_strdup(label);
+               break;
+       case G_GPIOSIM_CHIP_PROP_LINE_NAMES:
+               set_variant_prop(&self->line_names, val);
+               break;
+       case G_GPIOSIM_CHIP_PROP_HOGS:
+               set_variant_prop(&self->hogs, val);
+               break;
+       default:
+               G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, prop_id, pspec);
+               break;
+       }
+}
+
+static void g_gpiosim_chip_dispose(GObject *obj)
+{
+       GPIOSimChip *self = G_GPIOSIM_CHIP(obj);
+
+       g_clear_pointer(&self->line_names, g_variant_unref);
+       g_clear_pointer(&self->hogs, g_variant_unref);
+
+       G_OBJECT_CLASS(g_gpiosim_chip_parent_class)->dispose(obj);
+}
+
+static void g_gpiosim_disable_and_cleanup(struct gpiosim_bank *bank)
+{
+       struct gpiosim_dev *dev;
+       gint ret;
+
+       dev = gpiosim_bank_get_dev(bank);
+
+       if (gpiosim_dev_is_live(dev)) {
+               ret = gpiosim_dev_disable(dev);
+               if (ret)
+                       g_warning("Error while trying to disable the simulated GPIO device: %s",
+                                 g_strerror(errno));
+       }
+
+       gpiosim_dev_unref(dev);
+       gpiosim_bank_unref(bank);
+}
+
+static void g_gpiosim_chip_finalize(GObject *obj)
+{
+       GPIOSimChip *self = G_GPIOSIM_CHIP(obj);
+
+       g_clear_error(&self->construct_err);
+       g_clear_pointer(&self->label, g_free);
+       g_clear_pointer(&self->bank, g_gpiosim_disable_and_cleanup);
+
+       G_OBJECT_CLASS(g_gpiosim_chip_parent_class)->finalize(obj);
+}
+
+static void g_gpiosim_chip_class_init(GPIOSimChipClass *chip_class)
+{
+       GObjectClass *class = G_OBJECT_CLASS(chip_class);
+
+       class->constructed = g_gpiosim_chip_constructed;
+       class->get_property = g_gpiosim_chip_get_property;
+       class->set_property = g_gpiosim_chip_set_property;
+       class->dispose = g_gpiosim_chip_dispose;
+       class->finalize = g_gpiosim_chip_finalize;
+
+       g_object_class_install_property(
+               class, G_GPIOSIM_CHIP_PROP_DEV_PATH,
+               g_param_spec_string("dev-path", "Device path",
+                                   "Character device filesystem path.", NULL,
+                                   G_PARAM_READABLE));
+
+       g_object_class_install_property(
+               class, G_GPIOSIM_CHIP_PROP_NAME,
+               g_param_spec_string(
+                       "name", "Chip name",
+                       "Name of this chip device as set by the kernel.", NULL,
+                       G_PARAM_READABLE));
+
+       g_object_class_install_property(
+               class, G_GPIOSIM_CHIP_PROP_NUM_LINES,
+               g_param_spec_uint("num-lines", "Number of lines",
+                                 "Number of lines this simulated chip exposes.",
+                                 1, G_MAXUINT, 1,
+                                 G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY));
+
+       g_object_class_install_property(
+               class, G_GPIOSIM_CHIP_PROP_LABEL,
+               g_param_spec_string("label", "Chip label",
+                                   "Label of this simulated chip.", NULL,
+                                   G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY));
+
+       g_object_class_install_property(
+               class, G_GPIOSIM_CHIP_PROP_LINE_NAMES,
+               g_param_spec_variant(
+                       "line-names", "Line names",
+                       "List of names of the lines exposed by this chip",
+                       (GVariantType *)"a(us)", NULL,
+                       G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY));
+
+       g_object_class_install_property(
+               class, G_GPIOSIM_CHIP_PROP_HOGS,
+               g_param_spec_variant(
+                       "hogs", "Line hogs",
+                       "List of hogged lines and their directions.",
+                       (GVariantType *)"a(usi)", NULL,
+                       G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY));
+}
+
+static void g_gpiosim_chip_init(GPIOSimChip *self)
+{
+       self->construct_err = NULL;
+       self->num_lines = 1;
+       self->label = NULL;
+       self->line_names = NULL;
+       self->hogs = NULL;
+}
+
+static const gchar *
+g_gpiosim_chip_get_string_prop(GPIOSimChip *self, const gchar *prop)
+{
+       GValue val = G_VALUE_INIT;
+       const gchar *str;
+
+       g_object_get_property(G_OBJECT(self), prop, &val);
+       str = g_value_get_string(&val);
+       g_value_unset(&val);
+
+       return str;
+}
+
+const gchar *g_gpiosim_chip_get_dev_path(GPIOSimChip *self)
+{
+       return g_gpiosim_chip_get_string_prop(self, "dev-path");
+}
+
+const gchar *g_gpiosim_chip_get_name(GPIOSimChip *self)
+{
+       return g_gpiosim_chip_get_string_prop(self, "name");
+}
+
+GPIOSimValue
+_g_gpiosim_chip_get_value(GPIOSimChip *chip, guint offset, GError **err)
+{
+       enum gpiosim_value val;
+
+       val = gpiosim_bank_get_value(chip->bank, offset);
+       switch (val) {
+       case GPIOSIM_VALUE_ERROR:
+               g_set_error(err, G_GPIOSIM_ERROR,
+                           G_GPIOSIM_ERR_GET_VALUE_FAILED,
+                           "Unable to read the line value: %s",
+                           g_strerror(errno));
+               return G_GPIOSIM_VALUE_ERROR;
+       case GPIOSIM_VALUE_INACTIVE:
+               return G_GPIOSIM_VALUE_INACTIVE;
+       case GPIOSIM_VALUE_ACTIVE:
+               return G_GPIOSIM_VALUE_ACTIVE;
+       }
+
+       g_error("Invalid line value returned by gpiosim");
+}
+
+void g_gpiosim_chip_set_pull(GPIOSimChip *chip, guint offset, GPIOSimPull pull)
+{
+       enum gpiosim_pull sim_pull;
+       gint ret;
+
+       switch (pull) {
+       case G_GPIOSIM_PULL_DOWN:
+               sim_pull = GPIOSIM_PULL_DOWN;
+               break;
+       case G_GPIOSIM_PULL_UP:
+               sim_pull = GPIOSIM_PULL_UP;
+               break;
+       default:
+               g_error("invalid pull value");
+       }
+
+       ret = gpiosim_bank_set_pull(chip->bank, offset, sim_pull);
+       if (ret)
+               g_critical("Unable to set the pull setting for simulated line: %s",
+                           g_strerror(errno));
+}
diff --git a/tests/gpiod-test-sim.h b/tests/gpiod-test-sim.h
new file mode 100644 (file)
index 0000000..f6a4bf0
--- /dev/null
@@ -0,0 +1,79 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/* SPDX-FileCopyrightText: 2022 Bartosz Golaszewski <brgl@bgdev.pl> */
+
+#ifndef __GPIOD_TEST_SIM_H__
+#define __GPIOD_TEST_SIM_H__
+
+#include <gio/gio.h>
+#include <glib.h>
+#include <glib-object.h>
+
+G_BEGIN_DECLS
+
+typedef enum {
+       G_GPIOSIM_VALUE_ERROR = -1,
+       G_GPIOSIM_VALUE_INACTIVE = 0,
+       G_GPIOSIM_VALUE_ACTIVE = 1,
+} GPIOSimValue;
+
+typedef enum {
+       G_GPIOSIM_PULL_UP = 1,
+       G_GPIOSIM_PULL_DOWN,
+} GPIOSimPull;
+
+typedef enum {
+       G_GPIOSIM_DIRECTION_INPUT = 1,
+       G_GPIOSIM_DIRECTION_OUTPUT_HIGH,
+       G_GPIOSIM_DIRECTION_OUTPUT_LOW,
+} GPIOSimDirection;
+
+#define G_GPIOSIM_ERROR g_gpiosim_error_quark()
+
+typedef enum {
+       G_GPIOSIM_ERR_CTX_INIT_FAILED = 1,
+       G_GPIOSIM_ERR_CHIP_INIT_FAILED,
+       G_GPIOSIM_ERR_CHIP_ENABLE_FAILED,
+       G_GPIOSIM_ERR_GET_VALUE_FAILED,
+} GPIOSimError;
+
+GQuark g_gpiosim_error_quark(void);
+
+G_DECLARE_FINAL_TYPE(GPIOSimChip, g_gpiosim_chip, G_GPIOSIM, CHIP, GObject);
+
+#define G_GPIOSIM_CHIP_TYPE (g_gpiosim_chip_get_type())
+#define G_GPIOSIM_CHIP_OBJ(obj) \
+       (G_TYPE_CHECK_INSTANCE_CAST((obj), G_GPIOSIM_CHIP_TYPE, GPIOSimChip))
+
+#define g_gpiosim_chip_new(...) \
+       ({ \
+               g_autoptr(GError) _err = NULL; \
+               GPIOSimChip *_chip = G_GPIOSIM_CHIP_OBJ( \
+                                       g_initable_new(G_GPIOSIM_CHIP_TYPE, \
+                                                      NULL, &_err, \
+                                                      __VA_ARGS__)); \
+               g_assert_no_error(_err); \
+               if (g_test_failed()) \
+                       return; \
+               _chip; \
+       })
+
+const gchar *g_gpiosim_chip_get_dev_path(GPIOSimChip *self);
+const gchar *g_gpiosim_chip_get_name(GPIOSimChip *self);
+
+GPIOSimValue
+_g_gpiosim_chip_get_value(GPIOSimChip *self, guint offset, GError **err);
+void g_gpiosim_chip_set_pull(GPIOSimChip *self, guint offset, GPIOSimPull pull);
+
+#define g_gpiosim_chip_get_value(self, offset) \
+       ({ \
+               g_autoptr(GError) _err = NULL; \
+               gint _val = _g_gpiosim_chip_get_value(self, offset, &_err); \
+               g_assert_no_error(_err); \
+               if (g_test_failed()) \
+                       return; \
+               _val; \
+       })
+
+G_END_DECLS
+
+#endif /* __GPIOD_TEST_SIM_H__ */
diff --git a/tests/gpiod-test.c b/tests/gpiod-test.c
new file mode 100644 (file)
index 0000000..4e65ae2
--- /dev/null
@@ -0,0 +1,90 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+// SPDX-FileCopyrightText: 2017-2022 Bartosz Golaszewski <brgl@bgdev.pl>
+
+#include <errno.h>
+#include <linux/version.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <sys/utsname.h>
+#include <unistd.h>
+
+#include "gpiod-test.h"
+
+#define MIN_KERNEL_MAJOR       5
+#define MIN_KERNEL_MINOR       19
+#define MIN_KERNEL_RELEASE     0
+#define MIN_KERNEL_VERSION     KERNEL_VERSION(MIN_KERNEL_MAJOR, \
+                                              MIN_KERNEL_MINOR, \
+                                              MIN_KERNEL_RELEASE)
+
+static GList *tests;
+
+static gboolean check_kernel(void)
+{
+       guint major, minor, release;
+       struct utsname un;
+       gint ret;
+
+       g_debug("checking linux kernel version");
+
+       ret = uname(&un);
+       if (ret) {
+               g_critical("unable to read the kernel release version: %s",
+                          g_strerror(errno));
+               return FALSE;
+       }
+
+       ret = sscanf(un.release, "%u.%u.%u", &major, &minor, &release);
+       if (ret != 3) {
+               g_critical("error reading kernel release version");
+               return FALSE;
+       }
+
+       if (KERNEL_VERSION(major, minor, release) < MIN_KERNEL_VERSION) {
+               g_critical("linux kernel version must be at least v%u.%u.%u - got v%u.%u.%u",
+                          MIN_KERNEL_MAJOR, MIN_KERNEL_MINOR, MIN_KERNEL_RELEASE,
+                          major, minor, release);
+               return FALSE;
+       }
+
+       g_debug("kernel release is v%u.%u.%u - ok to run tests",
+               major, minor, release);
+
+       return TRUE;
+}
+
+static void test_func_wrapper(gconstpointer data)
+{
+       const struct _gpiod_test_case *test = data;
+
+       test->func();
+}
+
+static void add_test_from_list(gpointer element, gpointer data G_GNUC_UNUSED)
+{
+       struct _gpiod_test_case *test = element;
+
+       g_test_add_data_func(test->path, test, test_func_wrapper);
+}
+
+int main(int argc, char **argv)
+{
+       g_test_init(&argc, &argv, NULL);
+       g_test_set_nonfatal_assertions();
+
+       g_debug("running libgpiod test suite");
+       g_debug("%u tests registered", g_list_length(tests));
+
+       if (!check_kernel())
+               return EXIT_FAILURE;
+
+       g_list_foreach(tests, add_test_from_list, NULL);
+       g_list_free(tests);
+
+       return g_test_run();
+}
+
+void _gpiod_test_register(struct _gpiod_test_case *test)
+{
+       tests = g_list_append(tests, test);
+}
diff --git a/tests/gpiod-test.h b/tests/gpiod-test.h
new file mode 100644 (file)
index 0000000..6a84162
--- /dev/null
@@ -0,0 +1,45 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/* SPDX-FileCopyrightText: 2017-2022 Bartosz Golaszewski <brgl@bgdev.pl> */
+
+/*
+ * Testing framework for the core library.
+ *
+ * This file contains functions and definitions extending the GLib unit testing
+ * framework with functionalities necessary to test the libgpiod core C API as
+ * well as the kernel-to-user-space interface.
+ */
+
+#ifndef __GPIOD_TEST_H__
+#define __GPIOD_TEST_H__
+
+#include <glib.h>
+
+/* These are private definitions and should not be used directly. */
+
+struct _gpiod_test_case {
+       const gchar *path;
+       void (*func)(void);
+};
+
+void _gpiod_test_register(struct _gpiod_test_case *test);
+
+#define _GPIOD_TEST_PATH(_name) \
+               "/gpiod/" GPIOD_TEST_GROUP "/" G_STRINGIFY(_name)
+
+/*
+ * Register a test case function.
+ */
+#define GPIOD_TEST_CASE(_name) \
+       static void _gpiod_test_func_##_name(void); \
+       static struct _gpiod_test_case _##_name##_test_case = { \
+               .path = _GPIOD_TEST_PATH(_name), \
+               .func = _gpiod_test_func_##_name, \
+       }; \
+       static __attribute__((constructor)) void \
+       _##_name##_test_register(void) \
+       { \
+               _gpiod_test_register(&_##_name##_test_case); \
+       } \
+       static void _gpiod_test_func_##_name(void)
+
+#endif /* __GPIOD_TEST_H__ */
diff --git a/tests/gpiosim/.gitignore b/tests/gpiosim/.gitignore
new file mode 100644 (file)
index 0000000..5731644
--- /dev/null
@@ -0,0 +1,4 @@
+# SPDX-License-Identifier: GPL-2.0-or-later
+# SPDX-FileCopyrightText: 2017-2022 Bartosz Golaszewski <brgl@bgdev.pl>
+
+gpiosim-selftest
diff --git a/tests/gpiosim/Makefile.am b/tests/gpiosim/Makefile.am
new file mode 100644 (file)
index 0000000..5888873
--- /dev/null
@@ -0,0 +1,15 @@
+# SPDX-License-Identifier: GPL-2.0-or-later
+# SPDX-FileCopyrightText: 2021-2022 Bartosz Golaszewski <brgl@bgdev.pl>
+
+lib_LTLIBRARIES = libgpiosim.la
+noinst_PROGRAMS = gpiosim-selftest
+
+AM_CFLAGS = -Wall -Wextra -g -fvisibility=hidden -std=gnu89
+AM_CFLAGS += -include $(top_builddir)/config.h
+
+libgpiosim_la_SOURCES = gpiosim.c gpiosim.h
+libgpiosim_la_CFLAGS = $(AM_CFLAGS) $(KMOD_CFLAGS) $(MOUNT_CFLAGS)
+libgpiosim_la_LDFLAGS = -version-info $(subst .,:,$(ABI_GPIOSIM_VERSION))
+libgpiosim_la_LDFLAGS += $(KMOD_LIBS) $(MOUNT_LIBS) -pthread
+
+gpiosim_selftest_LDADD = libgpiosim.la
diff --git a/tests/gpiosim/gpiosim-selftest.c b/tests/gpiosim/gpiosim-selftest.c
new file mode 100644 (file)
index 0000000..ce6beee
--- /dev/null
@@ -0,0 +1,157 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+// SPDX-FileCopyrightText: 2021-2022 Bartosz Golaszewski <brgl@bgdev.pl>
+
+#include <stdio.h>
+#include <stdlib.h>
+
+#include "gpiosim.h"
+
+#define UNUSED __attribute__((unused))
+
+static const char *const line_names[] = {
+       "foo",
+       "bar",
+       "foobar",
+       NULL,
+       "barfoo",
+};
+
+int main(int argc UNUSED, char **argv UNUSED)
+{
+       struct gpiosim_bank *bank0, *bank1;
+       struct gpiosim_dev *dev;
+       struct gpiosim_ctx *ctx;
+       int ret, i;
+
+       printf("Creating gpiosim context\n");
+
+       ctx = gpiosim_ctx_new();
+       if (!ctx) {
+               perror("unable to create the gpios-sim context");
+               return EXIT_FAILURE;
+       }
+
+       printf("Creating a chip\n");
+
+       dev = gpiosim_dev_new(ctx);
+       if (!dev) {
+               perror("Unable to create a chip");
+               return EXIT_FAILURE;
+       }
+
+       printf("Creating a bank\n");
+
+       bank0 = gpiosim_bank_new(dev);
+       if (!bank0) {
+               perror("Unable to create a bank");
+               return EXIT_FAILURE;
+       }
+
+       printf("Creating a second bank\n");
+
+       bank1 = gpiosim_bank_new(dev);
+       if (!bank1) {
+               perror("Unable to create a bank");
+               return EXIT_FAILURE;
+       }
+
+       printf("Setting the label of bank #2 to foobar\n");
+
+       ret = gpiosim_bank_set_label(bank1, "foobar");
+       if (ret) {
+               perror("Unable to set the label of bank #2");
+               return EXIT_FAILURE;
+       }
+
+       printf("Setting the number of lines in bank #1 to 16\n");
+
+       ret = gpiosim_bank_set_num_lines(bank0, 16);
+       if (ret) {
+               perror("Unable to set the number of lines");
+               return EXIT_FAILURE;
+       }
+
+       printf("Setting the number of lines in bank #2 to 8\n");
+
+       ret = gpiosim_bank_set_num_lines(bank1, 8);
+       if (ret) {
+               perror("Unable to set the number of lines");
+               return EXIT_FAILURE;
+       }
+
+       printf("Setting names for some lines in bank #1\n");
+
+       for (i = 0; i < 5; i++) {
+               ret = gpiosim_bank_set_line_name(bank0, i, line_names[i]);
+               if (ret) {
+                       perror("Unable to set line names");
+                       return EXIT_FAILURE;
+               }
+       }
+
+       printf("Hog a line on bank #2\n");
+
+       ret = gpiosim_bank_hog_line(bank1, 3, "xyz",
+                                   GPIOSIM_DIRECTION_OUTPUT_HIGH);
+       if (ret) {
+               perror("Unable to hog a line");
+               return EXIT_FAILURE;
+       }
+
+       printf("Enabling the GPIO device\n");
+
+       ret = gpiosim_dev_enable(dev);
+       if (ret) {
+               perror("Unable to enable the device");
+               return EXIT_FAILURE;
+       }
+
+       printf("Setting the pull of a single line to pull-up\n");
+
+       ret = gpiosim_bank_set_pull(bank0, 6, GPIOSIM_PULL_UP);
+       if (ret) {
+               perror("Unable to set the pull");
+               return EXIT_FAILURE;
+       }
+
+       printf("Reading the pull back\n");
+
+       ret = gpiosim_bank_get_pull(bank0, 6);
+       if (ret < 0) {
+               perror("Unable to read the pull");
+               return EXIT_FAILURE;
+       }
+
+       if (ret != GPIOSIM_PULL_UP) {
+               fprintf(stderr, "Invalid pull value read\n");
+               return EXIT_FAILURE;
+       }
+
+       printf("Reading the value\n");
+
+       ret = gpiosim_bank_get_value(bank0, 6);
+       if (ret < 0) {
+               perror("Unable to read the value");
+               return EXIT_FAILURE;
+       }
+
+       if (ret != GPIOSIM_VALUE_ACTIVE) {
+               fprintf(stderr, "Invalid value read\n");
+               return EXIT_FAILURE;
+       }
+
+       printf("Disabling the GPIO device\n");
+
+       ret = gpiosim_dev_disable(dev);
+       if (ret) {
+               perror("Error while disabling the device");
+               return EXIT_FAILURE;
+       }
+
+       gpiosim_bank_unref(bank1);
+       gpiosim_bank_unref(bank0);
+       gpiosim_dev_unref(dev);
+       gpiosim_ctx_unref(ctx);
+
+       return EXIT_SUCCESS;
+}
diff --git a/tests/gpiosim/gpiosim.c b/tests/gpiosim/gpiosim.c
new file mode 100644 (file)
index 0000000..fca6b7f
--- /dev/null
@@ -0,0 +1,1178 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+// SPDX-FileCopyrightText: 2021-2022 Bartosz Golaszewski <brgl@bgdev.pl>
+
+#include <errno.h>
+#include <fcntl.h>
+#include <libkmod.h>
+#include <libmount.h>
+#include <linux/version.h>
+#include <pthread.h>
+#include <search.h>
+#include <stddef.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/mount.h>
+#include <sys/prctl.h>
+#include <sys/random.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <sys/utsname.h>
+#include <unistd.h>
+
+#include "gpiosim.h"
+
+#define GPIOSIM_API            __attribute__((visibility("default")))
+#define UNUSED                 __attribute__((unused))
+#define MIN_KERNEL_VERSION     KERNEL_VERSION(5, 17, 4)
+
+static pthread_mutex_t id_lock = PTHREAD_MUTEX_INITIALIZER;
+static pthread_once_t id_init_once = PTHREAD_ONCE_INIT;
+static void *id_root;
+
+struct {
+       int lowest;
+       bool found;
+} id_find_next_ctx;
+
+struct {
+       int id;
+       int *idp;
+} id_del_ctx;
+
+static void id_cleanup(void)
+{
+       tdestroy(id_root, free);
+}
+
+static void id_schedule_cleanup(void)
+{
+       atexit(id_cleanup);
+}
+
+static int id_compare(const void *p1, const void *p2)
+{
+       int id1 = *(int *)p1;
+       int id2 = *(int *)p2;
+
+       if (id1 < id2)
+               return -1;
+       if (id1 > id2)
+               return 1;
+       return 0;
+}
+
+static void id_find_next(const void *node, VISIT which, int depth UNUSED)
+{
+       int *id = *(int **)node;
+
+       if (id_find_next_ctx.found)
+               return;
+
+       switch (which) {
+       case postorder:
+       case leaf:
+               if (*id != id_find_next_ctx.lowest)
+                       id_find_next_ctx.found = true;
+               else
+                       id_find_next_ctx.lowest++;
+               break;
+       default:
+               break;
+       };
+}
+
+static void id_del(const void *node, VISIT which, int depth UNUSED)
+{
+       int *id = *(int **)node;
+
+       if (id_del_ctx.idp)
+               return;
+
+       switch (which) {
+       case postorder:
+       case leaf:
+               if (*id == id_del_ctx.id)
+                       id_del_ctx.idp = id;
+               break;
+       default:
+               break;
+       }
+}
+
+static int id_alloc(void)
+{
+       void *ret;
+       int *id;
+
+       pthread_once(&id_init_once, id_schedule_cleanup);
+
+       pthread_mutex_lock(&id_lock);
+
+       id_find_next_ctx.lowest = 0;
+       id_find_next_ctx.found = false;
+
+       twalk(id_root, id_find_next);
+
+       id = malloc(sizeof(*id));
+       if (!id) {
+               pthread_mutex_unlock(&id_lock);
+               return -1;
+       }
+
+       *id = id_find_next_ctx.lowest;
+
+       ret = tsearch(id, &id_root, id_compare);
+       if (!ret) {
+               pthread_mutex_unlock(&id_lock);
+               /* tsearch() doesn't set errno. */
+               errno = ENOMEM;
+               return -1;
+       }
+
+       pthread_mutex_unlock(&id_lock);
+
+       return *id;
+}
+
+static void id_free(int id)
+{
+       pthread_mutex_lock(&id_lock);
+
+       id_del_ctx.id = id;
+       id_del_ctx.idp = NULL;
+
+       twalk(id_root, id_del);
+       if (id_del_ctx.idp) {
+               tdelete(id_del_ctx.idp, &id_root, id_compare);
+               free(id_del_ctx.idp);
+       }
+
+       pthread_mutex_unlock(&id_lock);
+}
+
+struct refcount {
+       unsigned int cnt;
+       void (*release)(struct refcount *);
+};
+
+static void refcount_init(struct refcount *ref,
+                         void (*release)(struct refcount *))
+{
+       ref->cnt = 1;
+       ref->release = release;
+}
+
+static void refcount_inc(struct refcount *ref)
+{
+       ref->cnt++;
+}
+
+static void refcount_dec(struct refcount *ref)
+{
+       ref->cnt--;
+
+       if (!ref->cnt)
+               ref->release(ref);
+}
+
+struct list_head {
+       struct list_head *prev;
+       struct list_head *next;
+};
+
+static void list_init(struct list_head *list)
+{
+       list->next = list;
+       list->prev = list;
+}
+
+static void list_add(struct list_head *new, struct list_head *head)
+{
+       struct list_head *prev = head->prev;
+
+       head->prev = new;
+       new->next = head;
+       new->prev = prev;
+       prev->next = new;
+}
+
+static void list_del(struct list_head *entry)
+{
+       struct list_head *prev = entry->prev, *next = entry->next;
+
+       prev->next = next;
+       next->prev = prev;
+}
+
+#define container_of(ptr, type, member) ({ \
+       void *__mptr = (void *)(ptr); \
+       ((type *)(__mptr - offsetof(type, member))); \
+})
+
+#define list_entry(ptr, type, member) container_of(ptr, type, member)
+
+#define list_first_entry(ptr, type, member) \
+       list_entry((ptr)->next, type, member)
+
+#define list_next_entry(pos, member) \
+       list_entry((pos)->member.next, typeof(*(pos)), member)
+
+#define list_entry_is_head(pos, head, member) (&pos->member == (head))
+
+#define list_for_each_entry(pos, head, member) \
+       for (pos = list_first_entry(head, typeof(*pos), member); \
+            !list_entry_is_head(pos, head, member); \
+            pos = list_next_entry(pos, member))
+
+#define list_for_each_entry_safe(pos, next, head, member) \
+       for (pos = list_first_entry(head, typeof(*pos), member), \
+            next = list_next_entry(pos, member); \
+            !list_entry_is_head(pos, head, member); \
+            pos = next, next = list_next_entry(next, member))
+
+static int open_write_close(int base_fd, const char *where, const char *what)
+{
+       ssize_t written, size;
+       int fd;
+
+       if (what)
+               size = strlen(what) + 1;
+       else
+               size = 1;
+
+       fd = openat(base_fd, where, O_WRONLY);
+       if (fd < 0)
+               return -1;
+
+       written = write(fd, what ?: "", size);
+       close(fd);
+       if (written < 0) {
+               return -1;
+       } else if (written != size) {
+               errno = EIO;
+               return -1;
+       }
+
+       return 0;
+}
+
+static int open_read_close(int base_fd, const char *where,
+                          char *buf, size_t bufsize)
+{
+       ssize_t rd;
+       int fd;
+
+       fd = openat(base_fd, where, O_RDONLY);
+       if (fd < 0)
+               return -1;
+
+       memset(buf, 0, bufsize);
+       rd = read(fd, buf, bufsize);
+       close(fd);
+       if (rd < 0)
+               return -1;
+
+       if (buf[rd - 1] == '\n')
+               buf[rd - 1] = '\0';
+
+       return 0;
+}
+
+static int check_kernel_version(void)
+{
+       unsigned int major, minor, release;
+       struct utsname un;
+       int ret;
+
+       ret = uname(&un);
+       if (ret)
+               return -1;
+
+       ret = sscanf(un.release, "%u.%u.%u", &major, &minor, &release);
+       if (ret != 3) {
+               errno = EFAULT;
+               return -1;
+       }
+
+       if (KERNEL_VERSION(major, minor, release) < MIN_KERNEL_VERSION) {
+               errno = EOPNOTSUPP;
+               return -1;
+       }
+
+       return 0;
+}
+
+static int check_gpiosim_module(void)
+{
+       struct kmod_module *module;
+       struct kmod_ctx *kmod;
+       const char *modpath;
+       int ret, initstate;
+
+       kmod = kmod_new(NULL, NULL);
+       if (!kmod)
+               return -1;
+
+       ret = kmod_module_new_from_name(kmod, "gpio-sim", &module);
+       if (ret)
+               goto out_unref_kmod;
+
+again:
+       /* First check if the module is already loaded or built-in. */
+       initstate = kmod_module_get_initstate(module);
+       if (initstate < 0) {
+               if (errno == ENOENT) {
+                       /*
+                        * It's not loaded, let's see if we can do it manually.
+                        * See if we can find the module.
+                        */
+                       modpath = kmod_module_get_path(module);
+                       if (!modpath) {
+                               /* libkmod doesn't set errno. */
+                               errno = ENOENT;
+                               ret = -1;
+                               goto out_unref_module;
+                       }
+
+                       ret = kmod_module_probe_insert_module(
+                               module, KMOD_PROBE_IGNORE_LOADED, NULL, NULL,
+                               NULL, NULL);
+                       if (ret)
+                               goto out_unref_module;
+
+                       goto again;
+               } else {
+                       if (errno == 0)
+                               errno = EOPNOTSUPP;
+
+                       goto out_unref_module;
+               }
+       }
+
+       if (initstate != KMOD_MODULE_BUILTIN &&
+           initstate != KMOD_MODULE_LIVE &&
+           initstate != KMOD_MODULE_COMING) {
+               errno = EPERM;
+               goto out_unref_module;
+       }
+
+       ret = 0;
+
+out_unref_module:
+       kmod_module_unref(module);
+out_unref_kmod:
+       kmod_unref(kmod);
+       return ret;
+}
+
+static char *configfs_make_item(int at, int id)
+{
+       char *item_name, prname[17];
+       int ret;
+
+       ret = prctl(PR_GET_NAME, prname);
+       if (ret)
+               return NULL;
+
+       ret = asprintf(&item_name, "%s.%u.%d", prname, getpid(), id);
+       if (ret < 0)
+               return NULL;
+
+       ret = mkdirat(at, item_name, 0600);
+       if (ret) {
+               free(item_name);
+               return NULL;
+       }
+
+       return item_name;
+}
+
+struct gpiosim_ctx {
+       struct refcount refcnt;
+       int cfs_dir_fd;
+       char *cfs_mnt_dir;
+};
+
+struct gpiosim_dev {
+       struct refcount refcnt;
+       struct gpiosim_ctx *ctx;
+       bool live;
+       char *item_name;
+       int id;
+       char *dev_name;
+       int cfs_dir_fd;
+       int sysfs_dir_fd;
+       struct list_head banks;
+};
+
+struct gpiosim_bank {
+       struct refcount refcnt;
+       struct gpiosim_dev *dev;
+       struct list_head siblings;
+       char *item_name;
+       int id;
+       char *chip_name;
+       char *dev_path;
+       int cfs_dir_fd;
+       int sysfs_dir_fd;
+       size_t num_lines;
+       struct list_head lines;
+};
+
+struct gpiosim_line {
+       struct list_head siblings;
+       unsigned int offset;
+};
+
+static inline struct gpiosim_ctx *to_gpiosim_ctx(struct refcount *ref)
+{
+       return container_of(ref, struct gpiosim_ctx, refcnt);
+}
+
+static inline struct gpiosim_dev *to_gpiosim_dev(struct refcount *ref)
+{
+       return container_of(ref, struct gpiosim_dev, refcnt);
+}
+
+static inline struct gpiosim_bank *to_gpiosim_bank(struct refcount *ref)
+{
+       return container_of(ref, struct gpiosim_bank, refcnt);
+}
+
+static int ctx_open_configfs_dir(struct gpiosim_ctx *ctx, const char *cfs_path)
+{
+       char *path;
+       int ret;
+
+       ret = asprintf(&path, "%s/gpio-sim", cfs_path);
+       if (ret < 0)
+               return -1;
+
+       ctx->cfs_dir_fd = open(path, O_RDONLY);
+       free(path);
+       if (ctx->cfs_dir_fd < 0)
+               return -1;
+
+       return 0;
+}
+
+/*
+ * We don't need to check the configfs module as loading gpio-sim will pull it
+ * in but we need to find out if and where configfs was mounted. If it wasn't
+ * then as a last resort we'll try to mount it ourselves.
+ */
+static int ctx_get_configfs_fd(struct gpiosim_ctx *ctx)
+{
+       struct libmnt_context *mntctx;
+       struct libmnt_iter *iter;
+       struct libmnt_table *tb;
+       struct libmnt_fs *fs;
+       const char *type;
+       int ret;
+
+       /* Try to find out if and where configfs is mounted. */
+       mntctx = mnt_new_context();
+       if (!mntctx)
+               return -1;
+
+       ret = mnt_context_get_mtab(mntctx, &tb);
+       if (ret)
+               goto out_free_ctx;
+
+       iter = mnt_new_iter(MNT_ITER_FORWARD);
+       if (!iter)
+               goto out_free_ctx;
+
+       while (mnt_table_next_fs(tb, iter, &fs) == 0) {
+               type = mnt_fs_get_fstype(fs);
+
+               if (strcmp(type, "configfs") == 0) {
+                       ret = ctx_open_configfs_dir(ctx, mnt_fs_get_target(fs));
+                       if (ret)
+                               goto out_free_iter;
+
+                       ret = 0;
+                       goto out_free_iter;
+               }
+       }
+
+       /* Didn't find any configfs mounts - let's try to do it ourselves. */
+       ctx->cfs_mnt_dir = strdup("/tmp/gpiosim-configfs-XXXXXX");
+       if (!ctx->cfs_mnt_dir)
+               goto out_free_iter;
+
+       ctx->cfs_mnt_dir = mkdtemp(ctx->cfs_mnt_dir);
+       if (!ctx->cfs_mnt_dir)
+               goto out_free_tmpdir;
+
+       ret = mount(NULL, ctx->cfs_mnt_dir, "configfs", MS_RELATIME, NULL);
+       if (ret)
+               goto out_rm_tmpdir;
+
+       ret = ctx_open_configfs_dir(ctx, ctx->cfs_mnt_dir);
+       if (ret == 0)
+               /* Skip unmounting & deleting the tmp directory on success. */
+               goto out_free_iter;
+
+       umount(ctx->cfs_mnt_dir);
+out_rm_tmpdir:
+       rmdir(ctx->cfs_mnt_dir);
+out_free_tmpdir:
+       free(ctx->cfs_mnt_dir);
+       ctx->cfs_mnt_dir = NULL;
+out_free_iter:
+       mnt_free_iter(iter);
+out_free_ctx:
+       mnt_free_context(mntctx);
+
+       return ret;
+}
+
+static void ctx_release(struct refcount *ref)
+{
+       struct gpiosim_ctx *ctx = to_gpiosim_ctx(ref);
+
+       close(ctx->cfs_dir_fd);
+
+       if (ctx->cfs_mnt_dir) {
+               umount(ctx->cfs_mnt_dir);
+               rmdir(ctx->cfs_mnt_dir);
+               free(ctx->cfs_mnt_dir);
+       }
+
+       free(ctx);
+}
+
+GPIOSIM_API struct gpiosim_ctx *gpiosim_ctx_new(void)
+{
+       struct gpiosim_ctx *ctx;
+       int ret;
+
+       ret = check_kernel_version();
+       if (ret)
+               return NULL;
+
+       ret = check_gpiosim_module();
+       if (ret)
+               return NULL;
+
+       ctx = malloc(sizeof(*ctx));
+       if (!ctx)
+               return NULL;
+
+       memset(ctx, 0, sizeof(*ctx));
+       refcount_init(&ctx->refcnt, ctx_release);
+
+       ret = ctx_get_configfs_fd(ctx);
+       if (ret) {
+               free(ctx);
+               return NULL;
+       }
+
+       return ctx;
+}
+
+GPIOSIM_API struct gpiosim_ctx *gpiosim_ctx_ref(struct gpiosim_ctx *ctx)
+{
+       refcount_inc(&ctx->refcnt);
+
+       return ctx;
+}
+
+GPIOSIM_API void gpiosim_ctx_unref(struct gpiosim_ctx *ctx)
+{
+       refcount_dec(&ctx->refcnt);
+}
+
+static void dev_release(struct refcount *ref)
+{
+       struct gpiosim_dev *dev = to_gpiosim_dev(ref);
+       struct gpiosim_ctx *ctx = dev->ctx;
+
+       if (dev->live)
+               gpiosim_dev_disable(dev);
+
+       unlinkat(ctx->cfs_dir_fd, dev->item_name, AT_REMOVEDIR);
+       close(dev->cfs_dir_fd);
+       free(dev->dev_name);
+       free(dev->item_name);
+       id_free(dev->id);
+       gpiosim_ctx_unref(ctx);
+       free(dev);
+}
+
+GPIOSIM_API struct gpiosim_dev *gpiosim_dev_new(struct gpiosim_ctx *ctx)
+{
+       int configfs_fd, ret, id;
+       struct gpiosim_dev *dev;
+       char devname[128];
+       char *item_name;
+
+       id = id_alloc();
+       if (id < 0)
+               return NULL;
+
+       item_name = configfs_make_item(ctx->cfs_dir_fd, id);
+       if (!item_name)
+               goto err_free_id;
+
+       configfs_fd = openat(ctx->cfs_dir_fd, item_name, O_RDONLY);
+       if (configfs_fd < 0)
+               goto err_unlink;
+
+       dev = malloc(sizeof(*dev));
+       if (!dev)
+               goto err_close_fd;
+
+       ret = open_read_close(configfs_fd, "dev_name",
+                             devname, sizeof(devname));
+       if (ret)
+               goto err_free_dev;
+
+       memset(dev, 0, sizeof(*dev));
+       refcount_init(&dev->refcnt, dev_release);
+       list_init(&dev->banks);
+       dev->cfs_dir_fd = configfs_fd;
+       dev->sysfs_dir_fd = -1;
+       dev->item_name = item_name;
+       dev->id = id;
+
+       dev->dev_name = strdup(devname);
+       if (!dev->dev_name)
+               goto err_free_dev;
+
+       dev->ctx = gpiosim_ctx_ref(ctx);
+
+       return dev;
+
+err_free_dev:
+       free(dev);
+err_close_fd:
+       close(configfs_fd);
+err_unlink:
+       unlinkat(ctx->cfs_dir_fd, item_name, AT_REMOVEDIR);
+       free(item_name);
+err_free_id:
+       id_free(id);
+
+       return NULL;
+}
+
+GPIOSIM_API struct gpiosim_dev *gpiosim_dev_ref(struct gpiosim_dev *dev)
+{
+       refcount_inc(&dev->refcnt);
+
+       return dev;
+}
+
+GPIOSIM_API void gpiosim_dev_unref(struct gpiosim_dev *dev)
+{
+       refcount_dec(&dev->refcnt);
+}
+
+GPIOSIM_API struct gpiosim_ctx *gpiosim_dev_get_ctx(struct gpiosim_dev *dev)
+{
+       return gpiosim_ctx_ref(dev->ctx);
+}
+
+GPIOSIM_API const char *gpiosim_dev_get_name(struct gpiosim_dev *dev)
+{
+       return dev->dev_name;
+}
+
+static bool dev_check_pending(struct gpiosim_dev *dev)
+{
+       if (dev->live)
+               errno = EBUSY;
+
+       return !dev->live;
+}
+
+static bool dev_check_live(struct gpiosim_dev *dev)
+{
+       if (!dev->live)
+               errno = ENODEV;
+
+       return dev->live;
+}
+
+static int bank_set_chip_name(struct gpiosim_bank *bank)
+{
+       char chip_name[32];
+       int ret;
+
+       ret = open_read_close(bank->cfs_dir_fd, "chip_name",
+                             chip_name, sizeof(chip_name));
+       if (ret)
+               return -1;
+
+       bank->chip_name = strdup(chip_name);
+       if (!bank->chip_name)
+               return -1;
+
+       return 0;
+}
+
+static int bank_set_dev_path(struct gpiosim_bank *bank)
+{
+       char dev_path[64];
+
+       snprintf(dev_path, sizeof(dev_path), "/dev/%s", bank->chip_name);
+
+       bank->dev_path = strdup(dev_path);
+       if (!bank->dev_path)
+               return -1;
+
+       return 0;
+}
+
+static int bank_open_sysfs_dir(struct gpiosim_bank *bank)
+{
+       struct gpiosim_dev *dev = bank->dev;
+       int fd;
+
+       fd = openat(dev->sysfs_dir_fd, bank->chip_name, O_RDONLY);
+       if (fd < 0)
+               return -1;
+
+       bank->sysfs_dir_fd = fd;
+
+       return 0;
+}
+
+static int bank_enable(struct gpiosim_bank *bank)
+{
+       int ret;
+
+       ret = bank_set_chip_name(bank);
+       if (ret)
+               return -1;
+
+       ret = bank_set_dev_path(bank);
+       if (ret)
+               return -1;
+
+       return bank_open_sysfs_dir(bank);
+}
+
+static int dev_open_sysfs_dir(struct gpiosim_dev *dev)
+{
+       int ret, fd;
+       char *sysp;
+
+       ret = asprintf(&sysp, "/sys/devices/platform/%s", dev->dev_name);
+       if (ret < 0)
+               return -1;
+
+       fd = open(sysp, O_RDONLY);
+       free(sysp);
+       if (fd < 0)
+               return -1;
+
+       dev->sysfs_dir_fd = fd;
+
+       return 0;
+}
+
+/* Closes the sysfs dir for this device and all its child banks. */
+static void dev_close_sysfs_dirs(struct gpiosim_dev *dev)
+{
+       struct gpiosim_bank *bank;
+
+       list_for_each_entry(bank, &dev->banks, siblings) {
+               free(bank->chip_name);
+               free(bank->dev_path);
+               bank->chip_name = bank->dev_path = NULL;
+               close(bank->sysfs_dir_fd);
+               bank->sysfs_dir_fd = -1;
+       }
+
+       close(dev->sysfs_dir_fd);
+       dev->sysfs_dir_fd = -1;
+}
+
+GPIOSIM_API int gpiosim_dev_enable(struct gpiosim_dev *dev)
+{
+       struct gpiosim_bank *bank;
+       int ret;
+
+       if (!dev_check_pending(dev))
+               return -1;
+
+       ret = open_write_close(dev->cfs_dir_fd, "live", "1");
+       if (ret)
+               return -1;
+
+       ret = dev_open_sysfs_dir(dev);
+       if (ret) {
+               open_write_close(dev->cfs_dir_fd, "live", "0");
+               return -1;
+       }
+
+       bank = container_of(&dev->banks, struct gpiosim_bank, siblings);
+
+       list_for_each_entry(bank, &dev->banks, siblings) {
+               ret = bank_enable(bank);
+               if (ret) {
+                       dev_close_sysfs_dirs(dev);
+                       open_write_close(dev->cfs_dir_fd, "live", "0");
+                       return -1;
+               }
+       }
+
+       dev->live = true;
+
+       return 0;
+}
+
+GPIOSIM_API int gpiosim_dev_disable(struct gpiosim_dev *dev)
+{
+       int ret;
+
+       if (!dev_check_live(dev))
+               return -1;
+
+       ret = open_write_close(dev->cfs_dir_fd, "live", "0");
+       if (ret)
+               return ret;
+
+       dev_close_sysfs_dirs(dev);
+
+       dev->live = false;
+
+       return 0;
+}
+
+GPIOSIM_API bool gpiosim_dev_is_live(struct gpiosim_dev *dev)
+{
+       return dev->live;
+}
+
+static void bank_release(struct refcount *ref)
+{
+       struct gpiosim_bank *bank = to_gpiosim_bank(ref);
+       struct gpiosim_dev *dev = bank->dev;
+       struct gpiosim_line *line, *tmp;
+       char buf[64];
+
+       list_for_each_entry_safe(line, tmp, &bank->lines, siblings) {
+               snprintf(buf, sizeof(buf), "line%u/hog", line->offset);
+               unlinkat(bank->cfs_dir_fd, buf, AT_REMOVEDIR);
+
+               snprintf(buf, sizeof(buf), "line%u", line->offset);
+               unlinkat(bank->cfs_dir_fd, buf, AT_REMOVEDIR);
+
+               list_del(&line->siblings);
+               free(line);
+       }
+
+       list_del(&bank->siblings);
+       close(bank->cfs_dir_fd);
+       unlinkat(dev->cfs_dir_fd, bank->item_name, AT_REMOVEDIR);
+       gpiosim_dev_unref(dev);
+       if (bank->sysfs_dir_fd >= 0)
+               /* If the device wasn't disabled yet, this fd is still open. */
+               close(bank->sysfs_dir_fd);
+       free(bank->item_name);
+       id_free(bank->id);
+       free(bank->chip_name);
+       free(bank->dev_path);
+       free(bank);
+}
+
+GPIOSIM_API struct gpiosim_bank *gpiosim_bank_new(struct gpiosim_dev *dev)
+{
+       struct gpiosim_bank *bank;
+       int configfs_fd, id;
+       char *item_name;
+
+       if (!dev_check_pending(dev))
+               return NULL;
+
+       id = id_alloc();
+       if (id < 0)
+               return NULL;
+
+       item_name = configfs_make_item(dev->cfs_dir_fd, id);
+       if (!item_name)
+               goto err_free_id;
+
+       configfs_fd = openat(dev->cfs_dir_fd, item_name, O_RDONLY);
+       if (configfs_fd < 0)
+               goto err_unlink;
+
+       bank = malloc(sizeof(*bank));
+       if (!bank)
+               goto err_close_cfs;
+
+       memset(bank, 0, sizeof(*bank));
+
+       refcount_init(&bank->refcnt, bank_release);
+       list_add(&bank->siblings, &dev->banks);
+       bank->cfs_dir_fd = configfs_fd;
+       bank->dev = gpiosim_dev_ref(dev);
+       bank->item_name = item_name;
+       bank->num_lines = 1;
+       bank->id = id;
+       list_init(&bank->lines);
+
+       return bank;
+
+err_close_cfs:
+       close(configfs_fd);
+err_unlink:
+       unlinkat(dev->cfs_dir_fd, item_name, AT_REMOVEDIR);
+err_free_id:
+       id_free(id);
+
+       return NULL;
+}
+
+GPIOSIM_API struct gpiosim_bank *gpiosim_bank_ref(struct gpiosim_bank *bank)
+{
+       refcount_inc(&bank->refcnt);
+
+       return bank;
+}
+
+GPIOSIM_API void gpiosim_bank_unref(struct gpiosim_bank *bank)
+{
+       refcount_dec(&bank->refcnt);
+}
+
+GPIOSIM_API struct gpiosim_dev *gpiosim_bank_get_dev(struct gpiosim_bank *bank)
+{
+       return gpiosim_dev_ref(bank->dev);
+}
+
+GPIOSIM_API const char *gpiosim_bank_get_chip_name(struct gpiosim_bank *bank)
+{
+       return bank->chip_name;
+}
+
+GPIOSIM_API const char *gpiosim_bank_get_dev_path(struct gpiosim_bank *bank)
+{
+       return bank->dev_path;
+}
+
+GPIOSIM_API int gpiosim_bank_set_label(struct gpiosim_bank *bank,
+                                      const char *label)
+{
+       if (!dev_check_pending(bank->dev))
+               return -1;
+
+       return open_write_close(bank->cfs_dir_fd, "label", label);
+}
+
+GPIOSIM_API int gpiosim_bank_set_num_lines(struct gpiosim_bank *bank,
+                                          size_t num_lines)
+{
+       char buf[32];
+       int ret;
+
+       if (!dev_check_pending(bank->dev))
+               return -1;
+
+       snprintf(buf, sizeof(buf), "%zu", num_lines);
+
+       ret = open_write_close(bank->cfs_dir_fd, "num_lines", buf);
+       if (ret)
+               return -1;
+
+       bank->num_lines = num_lines;
+
+       return 0;
+}
+
+static int bank_make_line_dir(struct gpiosim_bank *bank, unsigned int offset)
+{
+       struct gpiosim_line *line;
+       char buf[32];
+       int ret;
+
+       snprintf(buf, sizeof(buf), "line%u", offset);
+
+       ret = faccessat(bank->cfs_dir_fd, buf, W_OK, 0);
+       if (!ret)
+               return 0;
+       if (ret && errno != ENOENT)
+               return -1;
+
+       line = malloc(sizeof(*line));
+       if (!line)
+               return -1;
+
+       ret = mkdirat(bank->cfs_dir_fd, buf, O_RDONLY);
+       if (ret) {
+               free(line);
+               return -1;
+       }
+
+       memset(line, 0, sizeof(*line));
+       line->offset = offset;
+       list_add(&line->siblings, &bank->lines);
+
+       return 0;
+}
+
+GPIOSIM_API int gpiosim_bank_set_line_name(struct gpiosim_bank *bank,
+                                          unsigned int offset,
+                                          const char *name)
+{
+       char buf[32];
+       int ret;
+
+       if (!dev_check_pending(bank->dev))
+               return -1;
+
+       ret = bank_make_line_dir(bank, offset);
+       if (ret)
+               return -1;
+
+       snprintf(buf, sizeof(buf), "line%u/name", offset);
+
+       return open_write_close(bank->cfs_dir_fd, buf, name ?: "");
+}
+
+GPIOSIM_API int gpiosim_bank_hog_line(struct gpiosim_bank *bank,
+                                     unsigned int offset, const char *name,
+                                     enum gpiosim_direction direction)
+{
+       char buf[64], *dir;
+       int ret, fd;
+
+       switch (direction) {
+       case GPIOSIM_DIRECTION_INPUT:
+               dir = "input";
+               break;
+       case GPIOSIM_DIRECTION_OUTPUT_HIGH:
+               dir = "output-high";
+               break;
+       case GPIOSIM_DIRECTION_OUTPUT_LOW:
+               dir = "output-low";
+               break;
+       default:
+               errno = EINVAL;
+               return -1;
+       }
+
+       if (!dev_check_pending(bank->dev))
+               return -1;
+
+       ret = bank_make_line_dir(bank, offset);
+       if (ret)
+               return -1;
+
+       snprintf(buf, sizeof(buf), "line%u/hog", offset);
+
+       ret = faccessat(bank->cfs_dir_fd, buf, W_OK, 0);
+       if (ret) {
+               if (errno == ENOENT) {
+                       ret = mkdirat(bank->cfs_dir_fd, buf, O_RDONLY);
+                       if (ret)
+                               return -1;
+               } else {
+                       return -1;
+               }
+       }
+
+       fd = openat(bank->cfs_dir_fd, buf, O_RDONLY);
+       if (fd < 0)
+               return -1;
+
+       ret = open_write_close(fd, "name", name ?: "");
+       if (ret) {
+               close(fd);
+               return -1;
+       }
+
+       ret = open_write_close(fd, "direction", dir);
+       close(fd);
+       return ret;
+}
+
+GPIOSIM_API int gpiosim_bank_clear_hog(struct gpiosim_bank *bank,
+                                      unsigned int offset)
+{
+       char buf[64];
+
+       snprintf(buf, sizeof(buf), "line%u/hog", offset);
+
+       return unlinkat(bank->cfs_dir_fd, buf, AT_REMOVEDIR);
+}
+
+static int sysfs_read_bank_attr(struct gpiosim_bank *bank, unsigned int offset,
+                               const char *attr, char *buf, size_t bufsize)
+{
+       struct gpiosim_dev *dev = bank->dev;
+       char where[32];
+
+       if (!dev_check_live(dev))
+               return -1;
+
+       snprintf(where, sizeof(where), "sim_gpio%u/%s", offset, attr);
+
+       return open_read_close(bank->sysfs_dir_fd, where, buf, bufsize);
+}
+
+GPIOSIM_API enum
+gpiosim_value gpiosim_bank_get_value(struct gpiosim_bank *bank,
+                                    unsigned int offset)
+{
+       char what[3];
+       int ret;
+
+       ret = sysfs_read_bank_attr(bank, offset, "value", what, sizeof(what));
+       if (ret)
+               return GPIOSIM_VALUE_ERROR;
+
+       if (what[0] == '0')
+               return GPIOSIM_VALUE_INACTIVE;
+       if (what[0] == '1')
+               return GPIOSIM_VALUE_ACTIVE;
+
+       errno = EIO;
+       return GPIOSIM_VALUE_ERROR;
+}
+
+GPIOSIM_API enum gpiosim_pull
+gpiosim_bank_get_pull(struct gpiosim_bank *bank, unsigned int offset)
+{
+       char what[16];
+       int ret;
+
+       ret = sysfs_read_bank_attr(bank, offset, "pull", what, sizeof(what));
+       if (ret)
+               return GPIOSIM_PULL_ERROR;
+
+       if (strcmp(what, "pull-down") == 0)
+               return GPIOSIM_PULL_DOWN;
+       if (strcmp(what, "pull-up") == 0)
+               return GPIOSIM_PULL_UP;
+
+       errno = EIO;
+       return GPIOSIM_PULL_ERROR;
+}
+
+GPIOSIM_API int
+gpiosim_bank_set_pull(struct gpiosim_bank *bank,
+                     unsigned int offset, enum gpiosim_pull pull)
+{
+       struct gpiosim_dev *dev = bank->dev;
+       char where[32], what[16];
+
+       if (!dev_check_live(dev))
+               return -1;
+
+       if (pull != GPIOSIM_PULL_DOWN && pull != GPIOSIM_PULL_UP) {
+               errno = EINVAL;
+               return -1;
+       }
+
+       snprintf(where, sizeof(where), "sim_gpio%u/pull", offset);
+       snprintf(what, sizeof(what),
+                pull == GPIOSIM_PULL_DOWN ? "pull-down" : "pull-up");
+
+       return open_write_close(bank->sysfs_dir_fd, where, what);
+}
diff --git a/tests/gpiosim/gpiosim.h b/tests/gpiosim/gpiosim.h
new file mode 100644 (file)
index 0000000..7d75852
--- /dev/null
@@ -0,0 +1,76 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/* SPDX-FileCopyrightText: 2021-2022 Bartosz Golaszewski <brgl@bgdev.pl> */
+
+#ifndef __GPIOD_GPIOSIM_H__
+#define __GPIOD_GPIOSIM_H__
+
+#include <stdbool.h>
+#include <stddef.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+struct gpiosim_ctx;
+struct gpiosim_dev;
+struct gpiosim_bank;
+
+enum gpiosim_value {
+       GPIOSIM_VALUE_ERROR = -1,
+       GPIOSIM_VALUE_INACTIVE = 0,
+       GPIOSIM_VALUE_ACTIVE = 1,
+};
+
+enum gpiosim_pull {
+       GPIOSIM_PULL_ERROR = -1,
+       GPIOSIM_PULL_DOWN = 1,
+       GPIOSIM_PULL_UP,
+};
+
+enum gpiosim_direction {
+       GPIOSIM_DIRECTION_INPUT = 1,
+       GPIOSIM_DIRECTION_OUTPUT_HIGH,
+       GPIOSIM_DIRECTION_OUTPUT_LOW,
+};
+
+struct gpiosim_ctx *gpiosim_ctx_new(void);
+struct gpiosim_ctx *gpiosim_ctx_ref(struct gpiosim_ctx *ctx);
+void gpiosim_ctx_unref(struct gpiosim_ctx *ctx);
+
+struct gpiosim_dev *gpiosim_dev_new(struct gpiosim_ctx *ctx);
+struct gpiosim_dev *gpiosim_dev_ref(struct gpiosim_dev *dev);
+void gpiosim_dev_unref(struct gpiosim_dev *dev);
+struct gpiosim_ctx *gpiosim_dev_get_ctx(struct gpiosim_dev *dev);
+const char *gpiosim_dev_get_name(struct gpiosim_dev *dev);
+
+int gpiosim_dev_enable(struct gpiosim_dev *dev);
+int gpiosim_dev_disable(struct gpiosim_dev *dev);
+bool gpiosim_dev_is_live(struct gpiosim_dev *dev);
+
+struct gpiosim_bank *gpiosim_bank_new(struct gpiosim_dev *dev);
+struct gpiosim_bank *gpiosim_bank_ref(struct gpiosim_bank *bank);
+void gpiosim_bank_unref(struct gpiosim_bank *bank);
+struct gpiosim_dev *gpiosim_bank_get_dev(struct gpiosim_bank *bank);
+const char *gpiosim_bank_get_chip_name(struct gpiosim_bank *bank);
+const char *gpiosim_bank_get_dev_path(struct gpiosim_bank *bank);
+
+int gpiosim_bank_set_label(struct gpiosim_bank *bank, const char *label);
+int gpiosim_bank_set_num_lines(struct gpiosim_bank *bank, size_t num_lines);
+int gpiosim_bank_set_line_name(struct gpiosim_bank *bank,
+                              unsigned int offset, const char *name);
+int gpiosim_bank_hog_line(struct gpiosim_bank *bank, unsigned int offset,
+                         const char *name, enum gpiosim_direction direction);
+int gpiosim_bank_clear_hog(struct gpiosim_bank *bank, unsigned int offset);
+
+enum gpiosim_value
+gpiosim_bank_get_value(struct gpiosim_bank *bank, unsigned int offset);
+enum gpiosim_pull
+gpiosim_bank_get_pull(struct gpiosim_bank *bank, unsigned int offset);
+int gpiosim_bank_set_pull(struct gpiosim_bank *bank,
+                         unsigned int offset, enum gpiosim_pull pull);
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* __GPIOD_GPIOSIM_H__ */
diff --git a/tests/tests-chip-info.c b/tests/tests-chip-info.c
new file mode 100644 (file)
index 0000000..db76385
--- /dev/null
@@ -0,0 +1,50 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+// SPDX-FileCopyrightText: 2017-2022 Bartosz Golaszewski <bartekgola@gmail.com>
+
+#include <errno.h>
+#include <glib.h>
+#include <gpiod.h>
+
+#include "gpiod-test.h"
+#include "gpiod-test-helpers.h"
+#include "gpiod-test-sim.h"
+
+#define GPIOD_TEST_GROUP "chip-info"
+
+GPIOD_TEST_CASE(get_chip_info_name)
+{
+       g_autoptr(GPIOSimChip) sim = g_gpiosim_chip_new(NULL);
+       g_autoptr(struct_gpiod_chip) chip = NULL;
+       g_autoptr(struct_gpiod_chip_info) info = NULL;
+
+       chip = gpiod_test_open_chip_or_fail(g_gpiosim_chip_get_dev_path(sim));
+       info = gpiod_test_chip_get_info_or_fail(chip);
+
+       g_assert_cmpstr(gpiod_chip_info_get_name(info), ==,
+                       g_gpiosim_chip_get_name(sim));
+}
+
+GPIOD_TEST_CASE(get_chip_info_label)
+{
+       g_autoptr(GPIOSimChip) sim = g_gpiosim_chip_new("label", "foobar",
+                                                       NULL);
+       g_autoptr(struct_gpiod_chip) chip = NULL;
+       g_autoptr(struct_gpiod_chip_info) info = NULL;
+
+       chip = gpiod_test_open_chip_or_fail(g_gpiosim_chip_get_dev_path(sim));
+       info = gpiod_test_chip_get_info_or_fail(chip);
+
+       g_assert_cmpstr(gpiod_chip_info_get_label(info), ==, "foobar");
+}
+
+GPIOD_TEST_CASE(get_num_lines)
+{
+       g_autoptr(GPIOSimChip) sim = g_gpiosim_chip_new("num-lines", 16, NULL);
+       g_autoptr(struct_gpiod_chip) chip = NULL;
+       g_autoptr(struct_gpiod_chip_info) info = NULL;
+
+       chip = gpiod_test_open_chip_or_fail(g_gpiosim_chip_get_dev_path(sim));
+       info = gpiod_test_chip_get_info_or_fail(chip);
+
+       g_assert_cmpuint(gpiod_chip_info_get_num_lines(info), ==, 16);
+}
diff --git a/tests/tests-chip.c b/tests/tests-chip.c
new file mode 100644 (file)
index 0000000..815b4c7
--- /dev/null
@@ -0,0 +1,199 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+// SPDX-FileCopyrightText: 2017-2021 Bartosz Golaszewski <bartekgola@gmail.com>
+
+#include <errno.h>
+#include <glib.h>
+#include <gpiod.h>
+
+#include "gpiod-test.h"
+#include "gpiod-test-helpers.h"
+#include "gpiod-test-sim.h"
+
+#define GPIOD_TEST_GROUP "chip"
+
+GPIOD_TEST_CASE(open_chip_good)
+{
+       g_autoptr(GPIOSimChip) sim = g_gpiosim_chip_new(NULL);
+       g_autoptr(struct_gpiod_chip) chip = NULL;
+
+       chip = gpiod_chip_open(g_gpiosim_chip_get_dev_path(sim));
+       g_assert_nonnull(chip);
+}
+
+GPIOD_TEST_CASE(open_chip_nonexistent)
+{
+       g_autoptr(struct_gpiod_chip) chip = NULL;
+
+       chip = gpiod_chip_open("/dev/nonexistent");
+       g_assert_null(chip);
+       gpiod_test_expect_errno(ENOENT);
+}
+
+GPIOD_TEST_CASE(open_chip_not_a_character_device)
+{
+       g_autoptr(struct_gpiod_chip) chip = NULL;
+
+       chip = gpiod_chip_open("/tmp");
+       g_assert_null(chip);
+       gpiod_test_expect_errno(ENOTTY);
+}
+
+GPIOD_TEST_CASE(open_chip_not_a_gpio_device)
+{
+       g_autoptr(struct_gpiod_chip) chip = NULL;
+
+       chip = gpiod_chip_open("/dev/null");
+       g_assert_null(chip);
+       gpiod_test_expect_errno(ENODEV);
+}
+
+GPIOD_TEST_CASE(open_chip_null_path)
+{
+       g_autoptr(struct_gpiod_chip) chip = NULL;
+
+       chip = gpiod_chip_open(NULL);
+       g_assert_null(chip);
+       gpiod_test_expect_errno(EINVAL);
+}
+
+GPIOD_TEST_CASE(get_chip_path)
+{
+       g_autoptr(GPIOSimChip) sim = g_gpiosim_chip_new(NULL);
+       g_autoptr(struct_gpiod_chip) chip = NULL;
+       const gchar *path = g_gpiosim_chip_get_dev_path(sim);
+
+       chip = gpiod_test_open_chip_or_fail(path);
+
+       g_assert_cmpstr(gpiod_chip_get_path(chip), ==, path);
+}
+
+GPIOD_TEST_CASE(get_fd)
+{
+       g_autoptr(GPIOSimChip) sim = g_gpiosim_chip_new(NULL);
+       g_autoptr(struct_gpiod_chip) chip = NULL;
+
+       chip = gpiod_test_open_chip_or_fail(g_gpiosim_chip_get_dev_path(sim));
+
+       g_assert_cmpint(gpiod_chip_get_fd(chip), >=, 0);
+}
+
+GPIOD_TEST_CASE(find_line_bad)
+{
+       static const GPIOSimLineName names[] = {
+               { .offset = 1, .name = "foo", },
+               { .offset = 2, .name = "bar", },
+               { .offset = 4, .name = "baz", },
+               { .offset = 5, .name = "xyz", },
+               { }
+       };
+
+       g_autoptr(GPIOSimChip) sim = NULL;
+       g_autoptr(struct_gpiod_chip) chip = NULL;
+       g_autoptr(GVariant) vnames = gpiod_test_package_line_names(names);
+
+       sim = g_gpiosim_chip_new(
+                       "num-lines", 8,
+                       "line-names", vnames,
+                        NULL);
+
+       chip = gpiod_test_open_chip_or_fail(g_gpiosim_chip_get_dev_path(sim));
+
+       g_assert_cmpint(
+               gpiod_chip_get_line_offset_from_name(chip,
+                                                    "nonexistent"), ==, -1);
+       gpiod_test_expect_errno(ENOENT);
+}
+
+GPIOD_TEST_CASE(find_line_good)
+{
+       static const GPIOSimLineName names[] = {
+               { .offset = 1, .name = "foo", },
+               { .offset = 2, .name = "bar", },
+               { .offset = 4, .name = "baz", },
+               { .offset = 5, .name = "xyz", },
+               { }
+       };
+
+       g_autoptr(GPIOSimChip) sim = NULL;
+       g_autoptr(struct_gpiod_chip) chip = NULL;
+       g_autoptr(GVariant) vnames = gpiod_test_package_line_names(names);
+
+       sim = g_gpiosim_chip_new(
+                       "num-lines", 8,
+                       "line-names", vnames,
+                       NULL);
+
+       chip = gpiod_test_open_chip_or_fail(g_gpiosim_chip_get_dev_path(sim));
+
+       g_assert_cmpint(gpiod_chip_get_line_offset_from_name(chip, "baz"),
+                       ==, 4);
+}
+
+/* Verify that for duplicated line names, the first one is returned. */
+GPIOD_TEST_CASE(find_line_duplicate)
+{
+       static const GPIOSimLineName names[] = {
+               { .offset = 1, .name = "foo", },
+               { .offset = 2, .name = "baz", },
+               { .offset = 4, .name = "baz", },
+               { .offset = 5, .name = "xyz", },
+               { }
+       };
+
+       g_autoptr(GPIOSimChip) sim = NULL;
+       g_autoptr(struct_gpiod_chip) chip = NULL;
+       g_autoptr(GVariant) vnames = gpiod_test_package_line_names(names);
+
+       sim = g_gpiosim_chip_new(
+                       "num-lines", 8,
+                       "line-names", vnames,
+                       NULL);
+
+       chip = gpiod_test_open_chip_or_fail(g_gpiosim_chip_get_dev_path(sim));
+
+       g_assert_cmpint(gpiod_chip_get_line_offset_from_name(chip, "baz"),
+                       ==, 2);
+}
+
+GPIOD_TEST_CASE(find_line_non_standard_names)
+{
+       static const GPIOSimLineName names[] = {
+               { .offset = 1, .name = "with whitespace", },
+               { .offset = 2, .name = "[:-]chars", },
+               { .offset = 3, .name = "l", },
+               { .offset = 6, .name = "ALLCAPS", },
+               { }
+       };
+
+       g_autoptr(GVariant) vnames = gpiod_test_package_line_names(names);
+       g_autoptr(GPIOSimChip) sim = g_gpiosim_chip_new("num-lines", 8,
+                                                       "line-names", vnames,
+                                                       NULL);
+
+       g_autoptr(struct_gpiod_chip) chip = NULL;
+
+       chip = gpiod_test_open_chip_or_fail(g_gpiosim_chip_get_dev_path(sim));
+
+       g_assert_cmpint(gpiod_chip_get_line_offset_from_name(chip,
+                                                            "with whitespace"),
+                        ==, 1);
+       g_assert_cmpint(gpiod_chip_get_line_offset_from_name(chip, "[:-]chars"),
+                       ==, 2);
+       g_assert_cmpint(gpiod_chip_get_line_offset_from_name(chip, "l"),
+                       ==, 3);
+       g_assert_cmpint(gpiod_chip_get_line_offset_from_name(chip, "ALLCAPS"),
+                       ==, 6);
+}
+
+GPIOD_TEST_CASE(find_line_null_name)
+{
+       g_autoptr(GPIOSimChip) sim = g_gpiosim_chip_new(NULL);
+       g_autoptr(struct_gpiod_chip) chip = NULL;
+       gint ret;
+
+       chip = gpiod_test_open_chip_or_fail(g_gpiosim_chip_get_dev_path(sim));
+
+       ret = gpiod_chip_get_line_offset_from_name(chip, NULL);
+       g_assert_cmpint(ret, ==, -1);
+       gpiod_test_expect_errno(EINVAL);
+}
diff --git a/tests/tests-edge-event.c b/tests/tests-edge-event.c
new file mode 100644 (file)
index 0000000..b744ca5
--- /dev/null
@@ -0,0 +1,683 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+// SPDX-FileCopyrightText: 2017-2021 Bartosz Golaszewski <bartekgola@gmail.com>
+
+#include <glib.h>
+#include <gpiod.h>
+#include <poll.h>
+
+#include "gpiod-test.h"
+#include "gpiod-test-helpers.h"
+#include "gpiod-test-sim.h"
+
+#define GPIOD_TEST_GROUP "edge-event"
+
+GPIOD_TEST_CASE(edge_event_buffer_capacity)
+{
+       g_autoptr(struct_gpiod_edge_event_buffer) buffer = NULL;
+
+       buffer = gpiod_test_create_edge_event_buffer_or_fail(32);
+
+       g_assert_cmpuint(gpiod_edge_event_buffer_get_capacity(buffer), ==, 32);
+}
+
+GPIOD_TEST_CASE(edge_event_buffer_max_capacity)
+{
+       g_autoptr(struct_gpiod_edge_event_buffer) buffer = NULL;
+
+       buffer = gpiod_test_create_edge_event_buffer_or_fail(16 * 64 * 2);
+
+       g_assert_cmpuint(gpiod_edge_event_buffer_get_capacity(buffer),
+                        ==, 16 * 64);
+}
+
+GPIOD_TEST_CASE(edge_event_wait_timeout)
+{
+       static const guint offset = 4;
+
+       g_autoptr(GPIOSimChip) sim = g_gpiosim_chip_new("num-lines", 8, NULL);
+       g_autoptr(struct_gpiod_chip) chip = NULL;
+       g_autoptr(struct_gpiod_line_config) line_cfg = NULL;
+       g_autoptr(struct_gpiod_line_settings) settings = NULL;
+       g_autoptr(struct_gpiod_line_request) request = NULL;
+       gint ret;
+
+       chip = gpiod_test_open_chip_or_fail(g_gpiosim_chip_get_dev_path(sim));
+       settings = gpiod_test_create_line_settings_or_fail();
+       line_cfg = gpiod_test_create_line_config_or_fail();
+
+       gpiod_line_settings_set_edge_detection(settings, GPIOD_LINE_EDGE_BOTH);
+       gpiod_test_line_config_add_line_settings_or_fail(line_cfg, &offset, 1,
+                                                        settings);
+
+       request = gpiod_test_chip_request_lines_or_fail(chip, NULL, line_cfg);
+
+       ret = gpiod_line_request_wait_edge_events(request, 1000000);
+       g_assert_cmpint(ret, ==, 0);
+}
+
+GPIOD_TEST_CASE(cannot_request_lines_in_output_mode_with_edge_detection)
+{
+       static const guint offset = 4;
+
+       g_autoptr(GPIOSimChip) sim = g_gpiosim_chip_new("num-lines", 8, NULL);
+       g_autoptr(struct_gpiod_chip) chip = NULL;
+       g_autoptr(struct_gpiod_line_settings) settings = NULL;
+       g_autoptr(struct_gpiod_line_config) line_cfg = NULL;
+       g_autoptr(struct_gpiod_line_request) request = NULL;
+
+       chip = gpiod_test_open_chip_or_fail(g_gpiosim_chip_get_dev_path(sim));
+       settings = gpiod_test_create_line_settings_or_fail();
+       line_cfg = gpiod_test_create_line_config_or_fail();
+
+       gpiod_line_settings_set_edge_detection(settings, GPIOD_LINE_EDGE_BOTH);
+       gpiod_line_settings_set_direction(settings,
+                                         GPIOD_LINE_DIRECTION_OUTPUT);
+
+       gpiod_test_line_config_add_line_settings_or_fail(line_cfg, &offset, 1,
+                                                        settings);
+
+       request = gpiod_chip_request_lines(chip, NULL, line_cfg);
+       g_assert_null(request);
+       gpiod_test_expect_errno(EINVAL);
+}
+
+static gpointer falling_and_rising_edge_events(gpointer data)
+{
+       GPIOSimChip *sim = data;
+
+       g_usleep(1000);
+
+       g_gpiosim_chip_set_pull(sim, 2, G_GPIOSIM_PULL_UP);
+
+       g_usleep(1000);
+
+       g_gpiosim_chip_set_pull(sim, 2, G_GPIOSIM_PULL_DOWN);
+
+       return NULL;
+}
+
+GPIOD_TEST_CASE(read_both_events)
+{
+       static const guint offset = 2;
+
+       g_autoptr(GPIOSimChip) sim = g_gpiosim_chip_new("num-lines", 8, NULL);
+       g_autoptr(struct_gpiod_chip) chip = NULL;
+       g_autoptr(struct_gpiod_line_settings) settings = NULL;
+       g_autoptr(struct_gpiod_line_config) line_cfg = NULL;
+       g_autoptr(struct_gpiod_line_request) request = NULL;
+       g_autoptr(GThread) thread = NULL;
+       g_autoptr(struct_gpiod_edge_event_buffer) buffer = NULL;
+       struct gpiod_edge_event *event;
+       guint64 ts_rising, ts_falling;
+       gint ret;
+
+       chip = gpiod_test_open_chip_or_fail(g_gpiosim_chip_get_dev_path(sim));
+       settings = gpiod_test_create_line_settings_or_fail();
+       line_cfg = gpiod_test_create_line_config_or_fail();
+       buffer = gpiod_test_create_edge_event_buffer_or_fail(64);
+
+       gpiod_line_settings_set_direction(settings, GPIOD_LINE_DIRECTION_INPUT);
+       gpiod_line_settings_set_edge_detection(settings, GPIOD_LINE_EDGE_BOTH);
+
+       gpiod_test_line_config_add_line_settings_or_fail(line_cfg, &offset, 1,
+                                                        settings);
+
+       request = gpiod_test_chip_request_lines_or_fail(chip, NULL, line_cfg);
+
+       thread = g_thread_new("request-release",
+                             falling_and_rising_edge_events, sim);
+       g_thread_ref(thread);
+
+       /* First event. */
+
+       ret = gpiod_line_request_wait_edge_events(request, 1000000000);
+       g_assert_cmpint(ret, >, 0);
+       gpiod_test_join_thread_and_return_if_failed(thread);
+
+       ret = gpiod_line_request_read_edge_events(request, buffer, 1);
+       g_assert_cmpint(ret, ==, 1);
+       gpiod_test_join_thread_and_return_if_failed(thread);
+
+       g_assert_cmpuint(gpiod_edge_event_buffer_get_num_events(buffer), ==, 1);
+       event = gpiod_edge_event_buffer_get_event(buffer, 0);
+       g_assert_nonnull(event);
+       gpiod_test_join_thread_and_return_if_failed(thread);
+
+       g_assert_cmpint(gpiod_edge_event_get_event_type(event), ==,
+                       GPIOD_EDGE_EVENT_RISING_EDGE);
+       g_assert_cmpuint(gpiod_edge_event_get_line_offset(event), ==, 2);
+       ts_rising = gpiod_edge_event_get_timestamp_ns(event);
+
+       /* Second event. */
+
+       ret = gpiod_line_request_wait_edge_events(request, 1000000000);
+       g_assert_cmpint(ret, >, 0);
+       gpiod_test_join_thread_and_return_if_failed(thread);
+
+       ret = gpiod_line_request_read_edge_events(request, buffer, 1);
+       g_assert_cmpint(ret, ==, 1);
+       gpiod_test_join_thread_and_return_if_failed(thread);
+
+       g_assert_cmpuint(gpiod_edge_event_buffer_get_num_events(buffer), ==, 1);
+       event = gpiod_edge_event_buffer_get_event(buffer, 0);
+       g_assert_nonnull(event);
+       gpiod_test_join_thread_and_return_if_failed(thread);
+
+       g_assert_cmpint(gpiod_edge_event_get_event_type(event), ==,
+                       GPIOD_EDGE_EVENT_FALLING_EDGE);
+       g_assert_cmpuint(gpiod_edge_event_get_line_offset(event), ==, 2);
+       ts_falling = gpiod_edge_event_get_timestamp_ns(event);
+
+       g_thread_join(thread);
+
+       g_assert_cmpuint(ts_falling, >, ts_rising);
+}
+
+GPIOD_TEST_CASE(read_rising_edge_event)
+{
+       static const guint offset = 2;
+
+       g_autoptr(GPIOSimChip) sim = g_gpiosim_chip_new("num-lines", 8, NULL);
+       g_autoptr(struct_gpiod_chip) chip = NULL;
+       g_autoptr(struct_gpiod_line_settings) settings = NULL;
+       g_autoptr(struct_gpiod_line_config) line_cfg = NULL;
+       g_autoptr(struct_gpiod_line_request) request = NULL;
+       g_autoptr(GThread) thread = NULL;
+       g_autoptr(struct_gpiod_edge_event_buffer) buffer = NULL;
+       struct gpiod_edge_event *event;
+       gint ret;
+
+       chip = gpiod_test_open_chip_or_fail(g_gpiosim_chip_get_dev_path(sim));
+       settings = gpiod_test_create_line_settings_or_fail();
+       line_cfg = gpiod_test_create_line_config_or_fail();
+       buffer = gpiod_test_create_edge_event_buffer_or_fail(64);
+
+       gpiod_line_settings_set_direction(settings, GPIOD_LINE_DIRECTION_INPUT);
+       gpiod_line_settings_set_edge_detection(settings,
+                                              GPIOD_LINE_EDGE_RISING);
+
+       gpiod_test_line_config_add_line_settings_or_fail(line_cfg, &offset, 1,
+                                                        settings);
+
+       request = gpiod_test_chip_request_lines_or_fail(chip, NULL, line_cfg);
+
+       thread = g_thread_new("edge-generator",
+                             falling_and_rising_edge_events, sim);
+       g_thread_ref(thread);
+
+       /* First event. */
+
+       ret = gpiod_line_request_wait_edge_events(request, 1000000000);
+       g_assert_cmpint(ret, >, 0);
+       gpiod_test_join_thread_and_return_if_failed(thread);
+
+       ret = gpiod_line_request_read_edge_events(request, buffer, 1);
+       g_assert_cmpint(ret, ==, 1);
+       gpiod_test_join_thread_and_return_if_failed(thread);
+
+       g_assert_cmpuint(gpiod_edge_event_buffer_get_num_events(buffer), ==, 1);
+       event = gpiod_edge_event_buffer_get_event(buffer, 0);
+       g_assert_nonnull(event);
+       gpiod_test_join_thread_and_return_if_failed(thread);
+
+       g_assert_cmpint(gpiod_edge_event_get_event_type(event), ==,
+                       GPIOD_EDGE_EVENT_RISING_EDGE);
+       g_assert_cmpuint(gpiod_edge_event_get_line_offset(event), ==, 2);
+
+       /* Second event. */
+
+       ret = gpiod_line_request_wait_edge_events(request, 1000000);
+       g_assert_cmpint(ret, ==, 0); /* Time-out. */
+
+       g_thread_join(thread);
+}
+
+GPIOD_TEST_CASE(read_falling_edge_event)
+{
+       static const guint offset = 2;
+
+       g_autoptr(GPIOSimChip) sim = g_gpiosim_chip_new("num-lines", 8, NULL);
+       g_autoptr(struct_gpiod_chip) chip = NULL;
+       g_autoptr(struct_gpiod_line_settings) settings = NULL;
+       g_autoptr(struct_gpiod_line_config) line_cfg = NULL;
+       g_autoptr(struct_gpiod_line_request) request = NULL;
+       g_autoptr(GThread) thread = NULL;
+       g_autoptr(struct_gpiod_edge_event_buffer) buffer = NULL;
+       struct gpiod_edge_event *event;
+       gint ret;
+
+       chip = gpiod_test_open_chip_or_fail(g_gpiosim_chip_get_dev_path(sim));
+       settings = gpiod_test_create_line_settings_or_fail();
+       line_cfg = gpiod_test_create_line_config_or_fail();
+       buffer = gpiod_test_create_edge_event_buffer_or_fail(64);
+
+       gpiod_line_settings_set_direction(settings, GPIOD_LINE_DIRECTION_INPUT);
+       gpiod_line_settings_set_edge_detection(settings,
+                                              GPIOD_LINE_EDGE_FALLING);
+
+       gpiod_test_line_config_add_line_settings_or_fail(line_cfg, &offset, 1,
+                                                        settings);
+
+       request = gpiod_test_chip_request_lines_or_fail(chip, NULL, line_cfg);
+
+       thread = g_thread_new("request-release",
+                             falling_and_rising_edge_events, sim);
+       g_thread_ref(thread);
+
+       /* First event is the second generated. */
+
+       ret = gpiod_line_request_wait_edge_events(request, 1000000000);
+       g_assert_cmpint(ret, >, 0);
+       gpiod_test_join_thread_and_return_if_failed(thread);
+
+       ret = gpiod_line_request_read_edge_events(request, buffer, 1);
+       g_assert_cmpint(ret, ==, 1);
+       gpiod_test_join_thread_and_return_if_failed(thread);
+
+       g_assert_cmpuint(gpiod_edge_event_buffer_get_num_events(buffer), ==, 1);
+       event = gpiod_edge_event_buffer_get_event(buffer, 0);
+       g_assert_nonnull(event);
+       gpiod_test_join_thread_and_return_if_failed(thread);
+
+       g_assert_cmpint(gpiod_edge_event_get_event_type(event), ==,
+                       GPIOD_EDGE_EVENT_FALLING_EDGE);
+       g_assert_cmpuint(gpiod_edge_event_get_line_offset(event), ==, 2);
+
+       /* No more events. */
+
+       ret = gpiod_line_request_wait_edge_events(request, 1000000);
+       g_assert_cmpint(ret, ==, 0); /* Time-out. */
+
+       g_thread_join(thread);
+}
+
+GPIOD_TEST_CASE(read_rising_edge_event_polled)
+{
+       static const guint offset = 2;
+
+       g_autoptr(GPIOSimChip) sim = g_gpiosim_chip_new("num-lines", 8, NULL);
+       g_autoptr(struct_gpiod_chip) chip = NULL;
+       g_autoptr(struct_gpiod_line_settings) settings = NULL;
+       g_autoptr(struct_gpiod_line_config) line_cfg = NULL;
+       g_autoptr(struct_gpiod_line_request) request = NULL;
+       g_autoptr(GThread) thread = NULL;
+       g_autoptr(struct_gpiod_edge_event_buffer) buffer = NULL;
+       struct gpiod_edge_event *event;
+       struct timespec ts;
+       struct pollfd pfd;
+       gint ret, fd;
+
+       chip = gpiod_test_open_chip_or_fail(g_gpiosim_chip_get_dev_path(sim));
+       settings = gpiod_test_create_line_settings_or_fail();
+       line_cfg = gpiod_test_create_line_config_or_fail();
+       buffer = gpiod_test_create_edge_event_buffer_or_fail(64);
+
+       gpiod_line_settings_set_direction(settings, GPIOD_LINE_DIRECTION_INPUT);
+       gpiod_line_settings_set_edge_detection(settings,
+                                              GPIOD_LINE_EDGE_RISING);
+
+       gpiod_test_line_config_add_line_settings_or_fail(line_cfg, &offset, 1,
+                                                        settings);
+
+       request = gpiod_test_chip_request_lines_or_fail(chip, NULL, line_cfg);
+
+       thread = g_thread_new("edge-generator",
+                             falling_and_rising_edge_events, sim);
+       g_thread_ref(thread);
+
+       /* First event. */
+
+       fd = gpiod_line_request_get_fd(request);
+
+       memset(&pfd, 0, sizeof(pfd));
+       pfd.fd = fd;
+       pfd.events = POLLIN | POLLPRI;
+
+       ts.tv_sec = 1;
+       ts.tv_nsec = 0;
+
+       ret = ppoll(&pfd, 1, &ts, NULL);
+       g_assert_cmpint(ret, >, 0);
+       gpiod_test_join_thread_and_return_if_failed(thread);
+
+       ret = gpiod_line_request_read_edge_events(request, buffer, 1);
+       g_assert_cmpint(ret, ==, 1);
+       gpiod_test_join_thread_and_return_if_failed(thread);
+
+       g_assert_cmpuint(gpiod_edge_event_buffer_get_num_events(buffer), ==, 1);
+       event = gpiod_edge_event_buffer_get_event(buffer, 0);
+       g_assert_nonnull(event);
+       gpiod_test_join_thread_and_return_if_failed(thread);
+
+       g_assert_cmpint(gpiod_edge_event_get_event_type(event), ==,
+                       GPIOD_EDGE_EVENT_RISING_EDGE);
+       g_assert_cmpuint(gpiod_edge_event_get_line_offset(event), ==, 2);
+
+       /* Second event. */
+
+       ret = gpiod_line_request_wait_edge_events(request, 1000000);
+       g_assert_cmpint(ret, ==, 0); /* Time-out. */
+
+       g_thread_join(thread);
+}
+
+GPIOD_TEST_CASE(read_both_events_blocking)
+{
+       /*
+        * This time without polling so that the read gets a chance to block
+        * and we can make sure it doesn't immediately return an error.
+        */
+
+       static const guint offset = 2;
+
+       g_autoptr(GPIOSimChip) sim = g_gpiosim_chip_new("num-lines", 8, NULL);
+       g_autoptr(struct_gpiod_chip) chip = NULL;
+       g_autoptr(struct_gpiod_line_settings) settings = NULL;
+       g_autoptr(struct_gpiod_line_config) line_cfg = NULL;
+       g_autoptr(struct_gpiod_line_request) request = NULL;
+       g_autoptr(GThread) thread = NULL;
+       g_autoptr(struct_gpiod_edge_event_buffer) buffer = NULL;
+       struct gpiod_edge_event *event;
+       gint ret;
+
+       chip = gpiod_test_open_chip_or_fail(g_gpiosim_chip_get_dev_path(sim));
+       settings = gpiod_test_create_line_settings_or_fail();
+       line_cfg = gpiod_test_create_line_config_or_fail();
+       buffer = gpiod_test_create_edge_event_buffer_or_fail(64);
+
+       gpiod_line_settings_set_direction(settings, GPIOD_LINE_DIRECTION_INPUT);
+       gpiod_line_settings_set_edge_detection(settings, GPIOD_LINE_EDGE_BOTH);
+
+       gpiod_test_line_config_add_line_settings_or_fail(line_cfg, &offset, 1,
+                                                        settings);
+
+       request = gpiod_test_chip_request_lines_or_fail(chip, NULL, line_cfg);
+
+       thread = g_thread_new("request-release",
+                             falling_and_rising_edge_events, sim);
+       g_thread_ref(thread);
+
+       /* First event. */
+
+       ret = gpiod_line_request_read_edge_events(request, buffer, 1);
+       g_assert_cmpint(ret, ==, 1);
+       gpiod_test_join_thread_and_return_if_failed(thread);
+
+       g_assert_cmpuint(gpiod_edge_event_buffer_get_num_events(buffer), ==, 1);
+       event = gpiod_edge_event_buffer_get_event(buffer, 0);
+       g_assert_nonnull(event);
+       gpiod_test_join_thread_and_return_if_failed(thread);
+
+       g_assert_cmpint(gpiod_edge_event_get_event_type(event), ==,
+                       GPIOD_EDGE_EVENT_RISING_EDGE);
+       g_assert_cmpuint(gpiod_edge_event_get_line_offset(event), ==, 2);
+
+       /* Second event. */
+
+       ret = gpiod_line_request_read_edge_events(request, buffer, 1);
+       g_assert_cmpint(ret, ==, 1);
+       gpiod_test_join_thread_and_return_if_failed(thread);
+
+       g_assert_cmpuint(gpiod_edge_event_buffer_get_num_events(buffer), ==, 1);
+       event = gpiod_edge_event_buffer_get_event(buffer, 0);
+       g_assert_nonnull(event);
+       gpiod_test_join_thread_and_return_if_failed(thread);
+
+       g_assert_cmpint(gpiod_edge_event_get_event_type(event), ==,
+                       GPIOD_EDGE_EVENT_FALLING_EDGE);
+       g_assert_cmpuint(gpiod_edge_event_get_line_offset(event), ==, 2);
+
+       g_thread_join(thread);
+}
+
+static gpointer rising_edge_events_on_two_offsets(gpointer data)
+{
+       GPIOSimChip *sim = data;
+
+       g_usleep(1000);
+
+       g_gpiosim_chip_set_pull(sim, 2, G_GPIOSIM_PULL_UP);
+
+       g_usleep(1000);
+
+       g_gpiosim_chip_set_pull(sim, 3, G_GPIOSIM_PULL_UP);
+
+       return NULL;
+}
+
+GPIOD_TEST_CASE(seqno)
+{
+       static const guint offsets[] = { 2, 3 };
+
+       g_autoptr(GPIOSimChip) sim = g_gpiosim_chip_new("num-lines", 8, NULL);
+       g_autoptr(struct_gpiod_chip) chip = NULL;
+       g_autoptr(struct_gpiod_line_settings) settings = NULL;
+       g_autoptr(struct_gpiod_line_config) line_cfg = NULL;
+       g_autoptr(struct_gpiod_line_request) request = NULL;
+       g_autoptr(GThread) thread = NULL;
+       g_autoptr(struct_gpiod_edge_event_buffer) buffer = NULL;
+       struct gpiod_edge_event *event;
+       gint ret;
+
+       chip = gpiod_test_open_chip_or_fail(g_gpiosim_chip_get_dev_path(sim));
+       settings = gpiod_test_create_line_settings_or_fail();
+       line_cfg = gpiod_test_create_line_config_or_fail();
+       buffer = gpiod_test_create_edge_event_buffer_or_fail(64);
+
+       gpiod_line_settings_set_direction(settings, GPIOD_LINE_DIRECTION_INPUT);
+       gpiod_line_settings_set_edge_detection(settings, GPIOD_LINE_EDGE_BOTH);
+
+       gpiod_test_line_config_add_line_settings_or_fail(line_cfg, offsets, 2,
+                                                        settings);
+
+       request = gpiod_test_chip_request_lines_or_fail(chip, NULL, line_cfg);
+
+       thread = g_thread_new("request-release",
+                             rising_edge_events_on_two_offsets, sim);
+       g_thread_ref(thread);
+
+       /* First event. */
+
+       ret = gpiod_line_request_wait_edge_events(request, 1000000000);
+       g_assert_cmpint(ret, >, 0);
+       gpiod_test_join_thread_and_return_if_failed(thread);
+
+       ret = gpiod_line_request_read_edge_events(request, buffer, 1);
+       g_assert_cmpint(ret, ==, 1);
+       gpiod_test_join_thread_and_return_if_failed(thread);
+
+       g_assert_cmpuint(gpiod_edge_event_buffer_get_num_events(buffer), ==, 1);
+       event = gpiod_edge_event_buffer_get_event(buffer, 0);
+       g_assert_nonnull(event);
+       gpiod_test_join_thread_and_return_if_failed(thread);
+
+       g_assert_cmpuint(gpiod_edge_event_get_line_offset(event), ==, 2);
+       g_assert_cmpuint(gpiod_edge_event_get_global_seqno(event), ==, 1);
+       g_assert_cmpuint(gpiod_edge_event_get_line_seqno(event), ==, 1);
+
+       /* Second event. */
+
+       ret = gpiod_line_request_wait_edge_events(request, 1000000000);
+       g_assert_cmpint(ret, >, 0);
+       gpiod_test_join_thread_and_return_if_failed(thread);
+
+       ret = gpiod_line_request_read_edge_events(request, buffer, 1);
+       g_assert_cmpint(ret, ==, 1);
+       gpiod_test_join_thread_and_return_if_failed(thread);
+
+       g_assert_cmpuint(gpiod_edge_event_buffer_get_num_events(buffer), ==, 1);
+       event = gpiod_edge_event_buffer_get_event(buffer, 0);
+       g_assert_nonnull(event);
+       gpiod_test_join_thread_and_return_if_failed(thread);
+
+       g_assert_cmpuint(gpiod_edge_event_get_line_offset(event), ==, 3);
+       g_assert_cmpuint(gpiod_edge_event_get_global_seqno(event), ==, 2);
+       g_assert_cmpuint(gpiod_edge_event_get_line_seqno(event), ==, 1);
+
+       g_thread_join(thread);
+}
+
+GPIOD_TEST_CASE(event_copy)
+{
+       static const guint offset = 2;
+
+       g_autoptr(GPIOSimChip) sim = g_gpiosim_chip_new("num-lines", 8, NULL);
+       g_autoptr(struct_gpiod_chip) chip = NULL;
+       g_autoptr(struct_gpiod_line_settings) settings = NULL;
+       g_autoptr(struct_gpiod_line_config) line_cfg = NULL;
+       g_autoptr(struct_gpiod_line_request) request = NULL;
+       g_autoptr(struct_gpiod_edge_event_buffer) buffer = NULL;
+       g_autoptr(struct_gpiod_edge_event) copy = NULL;
+       struct gpiod_edge_event *event;
+       gint ret;
+
+       chip = gpiod_test_open_chip_or_fail(g_gpiosim_chip_get_dev_path(sim));
+       settings = gpiod_test_create_line_settings_or_fail();
+       line_cfg = gpiod_test_create_line_config_or_fail();
+       buffer = gpiod_test_create_edge_event_buffer_or_fail(64);
+
+       gpiod_line_settings_set_direction(settings, GPIOD_LINE_DIRECTION_INPUT);
+       gpiod_line_settings_set_edge_detection(settings, GPIOD_LINE_EDGE_BOTH);
+
+       gpiod_test_line_config_add_line_settings_or_fail(line_cfg, &offset, 1,
+                                                        settings);
+
+       request = gpiod_test_chip_request_lines_or_fail(chip, NULL, line_cfg);
+
+       g_gpiosim_chip_set_pull(sim, 2, G_GPIOSIM_PULL_UP);
+
+       ret = gpiod_line_request_wait_edge_events(request, 1000000000);
+       g_assert_cmpint(ret, >, 0);
+       gpiod_test_return_if_failed();
+
+       ret = gpiod_line_request_read_edge_events(request, buffer, 1);
+       g_assert_cmpint(ret, ==, 1);
+       gpiod_test_return_if_failed();
+
+       event = gpiod_edge_event_buffer_get_event(buffer, 0);
+       g_assert_nonnull(event);
+       gpiod_test_return_if_failed();
+
+       copy = gpiod_edge_event_copy(event);
+       g_assert_nonnull(copy);
+       g_assert_true(copy != event);
+}
+
+GPIOD_TEST_CASE(reading_more_events_than_the_queue_contains_doesnt_block)
+{
+       static const guint offset = 2;
+
+       g_autoptr(GPIOSimChip) sim = g_gpiosim_chip_new("num-lines", 8, NULL);
+       g_autoptr(struct_gpiod_chip) chip = NULL;
+       g_autoptr(struct_gpiod_line_settings) settings = NULL;
+       g_autoptr(struct_gpiod_line_config) line_cfg = NULL;
+       g_autoptr(struct_gpiod_line_request) request = NULL;
+       g_autoptr(struct_gpiod_edge_event_buffer) buffer = NULL;
+       gint ret;
+
+       chip = gpiod_test_open_chip_or_fail(g_gpiosim_chip_get_dev_path(sim));
+       settings = gpiod_test_create_line_settings_or_fail();
+       line_cfg = gpiod_test_create_line_config_or_fail();
+       buffer = gpiod_test_create_edge_event_buffer_or_fail(64);
+
+       gpiod_line_settings_set_direction(settings, GPIOD_LINE_DIRECTION_INPUT);
+       gpiod_line_settings_set_edge_detection(settings, GPIOD_LINE_EDGE_BOTH);
+
+       gpiod_test_line_config_add_line_settings_or_fail(line_cfg, &offset, 1,
+                                                        settings);
+
+       request = gpiod_test_chip_request_lines_or_fail(chip, NULL, line_cfg);
+
+       g_gpiosim_chip_set_pull(sim, 2, G_GPIOSIM_PULL_UP);
+       g_usleep(500);
+       g_gpiosim_chip_set_pull(sim, 2, G_GPIOSIM_PULL_DOWN);
+       g_usleep(500);
+       g_gpiosim_chip_set_pull(sim, 2, G_GPIOSIM_PULL_UP);
+       g_usleep(500);
+       g_gpiosim_chip_set_pull(sim, 2, G_GPIOSIM_PULL_DOWN);
+       g_usleep(500);
+       g_gpiosim_chip_set_pull(sim, 2, G_GPIOSIM_PULL_UP);
+       g_usleep(500);
+       g_gpiosim_chip_set_pull(sim, 2, G_GPIOSIM_PULL_DOWN);
+       g_usleep(500);
+       g_gpiosim_chip_set_pull(sim, 2, G_GPIOSIM_PULL_UP);
+       g_usleep(500);
+
+       ret = gpiod_line_request_read_edge_events(request, buffer, 12);
+       g_assert_cmpint(ret, ==, 7);
+       gpiod_test_return_if_failed();
+
+       ret = gpiod_line_request_wait_edge_events(request, 1000);
+       g_assert_cmpint(ret, ==, 0);
+       gpiod_test_return_if_failed();
+}
+
+GPIOD_TEST_CASE(null_buffer)
+{
+       static const guint offset = 2;
+
+       g_autoptr(GPIOSimChip) sim = g_gpiosim_chip_new("num-lines", 8, NULL);
+       g_autoptr(struct_gpiod_chip) chip = NULL;
+       g_autoptr(struct_gpiod_line_settings) settings = NULL;
+       g_autoptr(struct_gpiod_line_config) line_cfg = NULL;
+       g_autoptr(struct_gpiod_line_request) request = NULL;
+       gint ret;
+
+       chip = gpiod_test_open_chip_or_fail(g_gpiosim_chip_get_dev_path(sim));
+       settings = gpiod_test_create_line_settings_or_fail();
+       line_cfg = gpiod_test_create_line_config_or_fail();
+
+       gpiod_line_settings_set_direction(settings, GPIOD_LINE_DIRECTION_INPUT);
+       gpiod_line_settings_set_edge_detection(settings, GPIOD_LINE_EDGE_BOTH);
+
+       gpiod_test_line_config_add_line_settings_or_fail(line_cfg, &offset, 1,
+                                                        settings);
+
+       request = gpiod_test_chip_request_lines_or_fail(chip, NULL, line_cfg);
+
+       ret = gpiod_line_request_read_edge_events(request, NULL, 1);
+       g_assert_cmpint(ret, ==, -1);
+       gpiod_test_expect_errno(EINVAL);
+}
+
+GPIOD_TEST_CASE(get_edge_event_index_out_of_bounds)
+{
+       static const guint offset = 2;
+
+       g_autoptr(GPIOSimChip) sim = g_gpiosim_chip_new("num-lines", 8, NULL);
+       g_autoptr(struct_gpiod_chip) chip = NULL;
+       g_autoptr(struct_gpiod_line_settings) settings = NULL;
+       g_autoptr(struct_gpiod_line_config) line_cfg = NULL;
+       g_autoptr(struct_gpiod_line_request) request = NULL;
+       g_autoptr(struct_gpiod_edge_event_buffer) buffer = NULL;
+       struct gpiod_edge_event *event;
+       gint ret;
+
+       chip = gpiod_test_open_chip_or_fail(g_gpiosim_chip_get_dev_path(sim));
+       settings = gpiod_test_create_line_settings_or_fail();
+       line_cfg = gpiod_test_create_line_config_or_fail();
+       buffer = gpiod_test_create_edge_event_buffer_or_fail(64);
+
+       gpiod_line_settings_set_direction(settings, GPIOD_LINE_DIRECTION_INPUT);
+       gpiod_line_settings_set_edge_detection(settings, GPIOD_LINE_EDGE_BOTH);
+
+       gpiod_test_line_config_add_line_settings_or_fail(line_cfg, &offset, 1,
+                                                        settings);
+
+       request = gpiod_test_chip_request_lines_or_fail(chip, NULL, line_cfg);
+
+       g_gpiosim_chip_set_pull(sim, 2, G_GPIOSIM_PULL_UP);
+       g_usleep(500);
+       g_gpiosim_chip_set_pull(sim, 2, G_GPIOSIM_PULL_DOWN);
+       g_usleep(500);
+       g_gpiosim_chip_set_pull(sim, 2, G_GPIOSIM_PULL_UP);
+       g_usleep(500);
+
+       ret = gpiod_line_request_read_edge_events(request, buffer, 3);
+       g_assert_cmpint(ret, ==, 3);
+       gpiod_test_return_if_failed();
+
+       event = gpiod_edge_event_buffer_get_event(buffer, 5);
+       g_assert_null(event);
+       gpiod_test_expect_errno(EINVAL);
+}
diff --git a/tests/tests-info-event.c b/tests/tests-info-event.c
new file mode 100644 (file)
index 0000000..cbd9e9e
--- /dev/null
@@ -0,0 +1,299 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+// SPDX-FileCopyrightText: 2017-2021 Bartosz Golaszewski <bartekgola@gmail.com>
+
+#include <glib.h>
+#include <gpiod.h>
+#include <poll.h>
+
+#include "gpiod-test.h"
+#include "gpiod-test-helpers.h"
+#include "gpiod-test-sim.h"
+
+#define GPIOD_TEST_GROUP "info-event"
+
+GPIOD_TEST_CASE(watching_info_events_returns_line_info)
+{
+       g_autoptr(GPIOSimChip) sim = g_gpiosim_chip_new("num-lines", 8, NULL);
+       g_autoptr(struct_gpiod_chip) chip = NULL;
+       g_autoptr(struct_gpiod_line_info) info = NULL;
+
+       chip = gpiod_test_open_chip_or_fail(g_gpiosim_chip_get_dev_path(sim));
+       info = gpiod_test_chip_watch_line_info_or_fail(chip, 3);
+       g_assert_cmpuint(gpiod_line_info_get_offset(info), ==, 3);
+}
+
+GPIOD_TEST_CASE(try_offset_out_of_range)
+{
+       g_autoptr(GPIOSimChip) sim = g_gpiosim_chip_new("num-lines", 8, NULL);
+       g_autoptr(struct_gpiod_chip) chip = NULL;
+       g_autoptr(struct_gpiod_line_info) info = NULL;
+
+       chip = gpiod_test_open_chip_or_fail(g_gpiosim_chip_get_dev_path(sim));
+       info = gpiod_chip_watch_line_info(chip, 10);
+       g_assert_null(info);
+       gpiod_test_expect_errno(EINVAL);
+}
+
+GPIOD_TEST_CASE(event_timeout)
+{
+       g_autoptr(GPIOSimChip) sim = g_gpiosim_chip_new("num-lines", 8, NULL);
+       g_autoptr(struct_gpiod_chip) chip = NULL;
+       g_autoptr(struct_gpiod_line_info) info = NULL;
+       gint ret;
+
+       chip = gpiod_test_open_chip_or_fail(g_gpiosim_chip_get_dev_path(sim));
+       info = gpiod_test_chip_watch_line_info_or_fail(chip, 6);
+
+       ret = gpiod_chip_wait_info_event(chip, 100000000);
+       g_assert_cmpint(ret, ==, 0);
+}
+
+struct request_ctx {
+       const char *path;
+       guint offset;
+};
+
+static gpointer request_reconfigure_release_line(gpointer data)
+{
+       g_autoptr(struct_gpiod_line_settings) settings = NULL;
+       g_autoptr(struct_gpiod_line_config) line_cfg = NULL;
+       g_autoptr(struct_gpiod_line_request) request = NULL;
+       g_autoptr(struct_gpiod_chip) chip = NULL;
+       struct request_ctx *ctx = data;
+       gint ret;
+
+       chip = gpiod_chip_open(ctx->path);
+       g_assert_nonnull(chip);
+       if (g_test_failed())
+               return NULL;
+
+       line_cfg = gpiod_line_config_new();
+       g_assert_nonnull(line_cfg);
+       if (g_test_failed())
+               return NULL;
+
+       settings = gpiod_line_settings_new();
+       g_assert_nonnull(settings);
+       if (g_test_failed())
+               return NULL;
+
+       g_usleep(1000);
+
+       ret = gpiod_line_config_add_line_settings(line_cfg, &ctx->offset,
+                                                 1, settings);
+       g_assert_cmpint(ret, ==, 0);
+       if (g_test_failed())
+               return NULL;
+
+       request = gpiod_chip_request_lines(chip, NULL, line_cfg);
+       g_assert_nonnull(request);
+       if (g_test_failed())
+               return NULL;
+
+       g_usleep(1000);
+
+       gpiod_line_config_reset(line_cfg);
+       gpiod_line_settings_set_direction(settings,
+                                         GPIOD_LINE_DIRECTION_OUTPUT);
+       ret = gpiod_line_config_add_line_settings(line_cfg, &ctx->offset,
+                                                 1, settings);
+       g_assert_cmpint(ret, ==, 0);
+       if (g_test_failed())
+               return NULL;
+
+       ret = gpiod_line_request_reconfigure_lines(request, line_cfg);
+       g_assert_cmpint(ret, ==, 0);
+       if (g_test_failed())
+               return NULL;
+
+       g_usleep(1000);
+
+       gpiod_line_request_release(request);
+       request = NULL;
+
+       return NULL;
+}
+
+GPIOD_TEST_CASE(request_reconfigure_release_events)
+{
+       g_autoptr(GPIOSimChip) sim = g_gpiosim_chip_new("num-lines", 8, NULL);
+       g_autoptr(struct_gpiod_chip) chip = NULL;
+       g_autoptr(struct_gpiod_line_info) info = NULL;
+       g_autoptr(struct_gpiod_info_event) request_event = NULL;
+       g_autoptr(struct_gpiod_info_event) reconfigure_event = NULL;
+       g_autoptr(struct_gpiod_info_event) release_event = NULL;
+       g_autoptr(GThread) thread = NULL;
+       struct gpiod_line_info *request_info, *reconfigure_info, *release_info;
+       guint64 request_ts, reconfigure_ts, release_ts;
+       struct request_ctx ctx;
+       const char *chip_path = g_gpiosim_chip_get_dev_path(sim);
+       gint ret;
+
+       chip = gpiod_test_open_chip_or_fail(chip_path);
+       info = gpiod_test_chip_watch_line_info_or_fail(chip, 3);
+
+       g_assert_false(gpiod_line_info_is_used(info));
+
+       ctx.path = chip_path;
+       ctx.offset = 3;
+
+       thread = g_thread_new("request-release",
+                             request_reconfigure_release_line, &ctx);
+       g_thread_ref(thread);
+
+       ret = gpiod_chip_wait_info_event(chip, 1000000000);
+       g_assert_cmpint(ret, >, 0);
+       gpiod_test_join_thread_and_return_if_failed(thread);
+
+       request_event = gpiod_chip_read_info_event(chip);
+       g_assert_nonnull(request_event);
+       gpiod_test_join_thread_and_return_if_failed(thread);
+
+       g_assert_cmpint(gpiod_info_event_get_event_type(request_event), ==,
+                       GPIOD_INFO_EVENT_LINE_REQUESTED);
+
+       request_info = gpiod_info_event_get_line_info(request_event);
+
+       g_assert_cmpuint(gpiod_line_info_get_offset(request_info), ==, 3);
+       g_assert_true(gpiod_line_info_is_used(request_info));
+       g_assert_cmpint(gpiod_line_info_get_direction(request_info), ==,
+                       GPIOD_LINE_DIRECTION_INPUT);
+
+       ret = gpiod_chip_wait_info_event(chip, 1000000000);
+       g_assert_cmpint(ret, >, 0);
+       gpiod_test_join_thread_and_return_if_failed(thread);
+
+       reconfigure_event = gpiod_chip_read_info_event(chip);
+       g_assert_nonnull(reconfigure_event);
+       gpiod_test_join_thread_and_return_if_failed(thread);
+
+       g_assert_cmpint(gpiod_info_event_get_event_type(reconfigure_event), ==,
+                       GPIOD_INFO_EVENT_LINE_CONFIG_CHANGED);
+
+       reconfigure_info = gpiod_info_event_get_line_info(reconfigure_event);
+
+       g_assert_cmpuint(gpiod_line_info_get_offset(reconfigure_info), ==, 3);
+       g_assert_true(gpiod_line_info_is_used(reconfigure_info));
+       g_assert_cmpint(gpiod_line_info_get_direction(reconfigure_info), ==,
+                       GPIOD_LINE_DIRECTION_OUTPUT);
+
+       ret = gpiod_chip_wait_info_event(chip, 1000000000);
+       g_assert_cmpint(ret, >, 0);
+       gpiod_test_join_thread_and_return_if_failed(thread);
+
+       release_event = gpiod_chip_read_info_event(chip);
+       g_assert_nonnull(release_event);
+       gpiod_test_join_thread_and_return_if_failed(thread);
+
+       g_assert_cmpint(gpiod_info_event_get_event_type(release_event), ==,
+                       GPIOD_INFO_EVENT_LINE_RELEASED);
+
+       release_info = gpiod_info_event_get_line_info(release_event);
+
+       g_assert_cmpuint(gpiod_line_info_get_offset(release_info), ==, 3);
+       g_assert_false(gpiod_line_info_is_used(release_info));
+
+       g_thread_join(thread);
+
+       request_ts = gpiod_info_event_get_timestamp_ns(request_event);
+       reconfigure_ts = gpiod_info_event_get_timestamp_ns(reconfigure_event);
+       release_ts = gpiod_info_event_get_timestamp_ns(release_event);
+
+       g_assert_cmpuint(request_ts, <, reconfigure_ts);
+       g_assert_cmpuint(reconfigure_ts, <, release_ts);
+}
+
+GPIOD_TEST_CASE(chip_fd_can_be_polled)
+{
+       g_autoptr(GPIOSimChip) sim = g_gpiosim_chip_new("num-lines", 8, NULL);
+       g_autoptr(struct_gpiod_chip) chip = NULL;
+       g_autoptr(struct_gpiod_line_info) info = NULL;
+       g_autoptr(struct_gpiod_info_event) event = NULL;
+       g_autoptr(GThread) thread = NULL;
+       const char *chip_path = g_gpiosim_chip_get_dev_path(sim);
+       struct gpiod_line_info *evinfo;
+       struct request_ctx ctx;
+       struct timespec ts;
+       struct pollfd pfd;
+       gint ret, fd;
+
+       chip = gpiod_test_open_chip_or_fail(chip_path);
+       info = gpiod_test_chip_watch_line_info_or_fail(chip, 3);
+
+       g_assert_false(gpiod_line_info_is_used(info));
+
+       ctx.path = chip_path;
+       ctx.offset = 3;
+
+       thread = g_thread_new("request-release",
+                             request_reconfigure_release_line, &ctx);
+       g_thread_ref(thread);
+
+       fd = gpiod_chip_get_fd(chip);
+
+       memset(&pfd, 0, sizeof(pfd));
+       pfd.fd = fd;
+       pfd.events = POLLIN | POLLPRI;
+
+       ts.tv_sec = 1;
+       ts.tv_nsec = 0;
+
+       ret = ppoll(&pfd, 1, &ts, NULL);
+       g_assert_cmpint(ret, >, 0);
+       gpiod_test_join_thread_and_return_if_failed(thread);
+
+       event = gpiod_chip_read_info_event(chip);
+       g_assert_nonnull(event);
+       gpiod_test_join_thread_and_return_if_failed(thread);
+
+       g_assert_cmpint(gpiod_info_event_get_event_type(event), ==,
+                       GPIOD_INFO_EVENT_LINE_REQUESTED);
+
+       evinfo = gpiod_info_event_get_line_info(event);
+
+       g_assert_cmpuint(gpiod_line_info_get_offset(evinfo), ==, 3);
+       g_assert_true(gpiod_line_info_is_used(evinfo));
+
+       g_thread_join(thread);
+}
+
+GPIOD_TEST_CASE(unwatch_and_check_that_no_events_are_generated)
+{
+       static const guint offset = 3;
+
+       g_autoptr(GPIOSimChip) sim = g_gpiosim_chip_new("num-lines", 8, NULL);
+       g_autoptr(struct_gpiod_chip) chip = NULL;
+       g_autoptr(struct_gpiod_line_info) info = NULL;
+       g_autoptr(struct_gpiod_info_event) event = NULL;
+       g_autoptr(struct_gpiod_line_config) line_cfg = NULL;
+       g_autoptr(struct_gpiod_line_request) request = NULL;
+       gint ret;
+
+       chip = gpiod_test_open_chip_or_fail(g_gpiosim_chip_get_dev_path(sim));
+       line_cfg = gpiod_test_create_line_config_or_fail();
+
+       gpiod_test_line_config_add_line_settings_or_fail(line_cfg, &offset, 1,
+                                                        NULL);
+
+       info = gpiod_test_chip_watch_line_info_or_fail(chip, 3);
+
+       request = gpiod_test_chip_request_lines_or_fail(chip, NULL, line_cfg);
+
+       ret = gpiod_chip_wait_info_event(chip, 100000000);
+       g_assert_cmpint(ret, >, 0);
+       gpiod_test_return_if_failed();
+
+       event = gpiod_chip_read_info_event(chip);
+       g_assert_nonnull(event);
+       gpiod_test_return_if_failed();
+
+       ret = gpiod_chip_unwatch_line_info(chip, 3);
+       g_assert_cmpint(ret, ==, 0);
+       gpiod_test_return_if_failed();
+
+       gpiod_line_request_release(request);
+       request = NULL;
+
+       ret = gpiod_chip_wait_info_event(chip, 100000000);
+       g_assert_cmpint(ret, ==, 0);
+}
diff --git a/tests/tests-line-config.c b/tests/tests-line-config.c
new file mode 100644 (file)
index 0000000..469500b
--- /dev/null
@@ -0,0 +1,470 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+// SPDX-FileCopyrightText: 2017-2021 Bartosz Golaszewski <bartekgola@gmail.com>
+
+#include <errno.h>
+#include <glib.h>
+#include <gpiod.h>
+
+#include "gpiod-test.h"
+#include "gpiod-test-helpers.h"
+#include "gpiod-test-sim.h"
+
+#define GPIOD_TEST_GROUP "line-config"
+
+GPIOD_TEST_CASE(too_many_lines)
+{
+       g_autoptr(struct_gpiod_line_settings) settings = NULL;
+       g_autoptr(struct_gpiod_line_config) config = NULL;
+       guint offsets[65], i;
+       gint ret;
+
+       settings = gpiod_test_create_line_settings_or_fail();
+       config = gpiod_test_create_line_config_or_fail();
+
+       for (i = 0; i < 65; i++)
+               offsets[i] = i;
+
+       ret = gpiod_line_config_add_line_settings(config, offsets, 65,
+                                                 settings);
+       g_assert_cmpint(ret, <, 0);
+       g_assert_cmpint(errno, ==, E2BIG);
+}
+
+GPIOD_TEST_CASE(get_line_settings)
+{
+       static const guint offsets[] = { 0, 1, 2, 3 };
+
+       g_autoptr(struct_gpiod_line_settings) settings = NULL;
+       g_autoptr(struct_gpiod_line_settings) retrieved = NULL;
+       g_autoptr(struct_gpiod_line_config) config = NULL;
+
+       settings = gpiod_test_create_line_settings_or_fail();
+       config = gpiod_test_create_line_config_or_fail();
+
+       gpiod_line_settings_set_direction(settings, GPIOD_LINE_DIRECTION_INPUT);
+       gpiod_line_settings_set_bias(settings, GPIOD_LINE_BIAS_PULL_DOWN);
+       gpiod_test_line_config_add_line_settings_or_fail(config, offsets, 4,
+                                                        settings);
+
+       retrieved = gpiod_test_line_config_get_line_settings_or_fail(config, 2);
+
+       g_assert_cmpint(gpiod_line_settings_get_direction(retrieved), ==,
+                       GPIOD_LINE_DIRECTION_INPUT);
+       g_assert_cmpint(gpiod_line_settings_get_bias(retrieved), ==,
+                       GPIOD_LINE_BIAS_PULL_DOWN);
+}
+
+GPIOD_TEST_CASE(too_many_attrs)
+{
+       g_autoptr(GPIOSimChip) sim = g_gpiosim_chip_new("num-lines", 8, NULL);
+       g_autoptr(struct_gpiod_chip) chip = NULL;
+       g_autoptr(struct_gpiod_line_config) line_cfg = NULL;
+       g_autoptr(struct_gpiod_line_settings) settings = NULL;
+       g_autoptr(struct_gpiod_line_request) request = NULL;
+       guint offset;
+
+       chip = gpiod_test_open_chip_or_fail(g_gpiosim_chip_get_dev_path(sim));
+       settings = gpiod_test_create_line_settings_or_fail();
+       line_cfg = gpiod_test_create_line_config_or_fail();
+
+       gpiod_line_settings_set_direction(settings,
+                                         GPIOD_LINE_DIRECTION_OUTPUT);
+       gpiod_line_settings_set_output_value(settings, GPIOD_LINE_VALUE_ACTIVE);
+       offset = 0;
+       gpiod_test_line_config_add_line_settings_or_fail(line_cfg, &offset, 1,
+                                                        settings);
+
+       gpiod_line_settings_set_direction(settings, GPIOD_LINE_DIRECTION_INPUT);
+       gpiod_line_settings_set_debounce_period_us(settings, 1000);
+       gpiod_line_settings_set_edge_detection(settings, GPIOD_LINE_EDGE_BOTH);
+       offset = 1;
+       gpiod_test_line_config_add_line_settings_or_fail(line_cfg, &offset, 1,
+                                                        settings);
+
+       gpiod_line_settings_set_bias(settings, GPIOD_LINE_BIAS_PULL_UP);
+       offset = 2;
+       gpiod_test_line_config_add_line_settings_or_fail(line_cfg, &offset, 1,
+                                                        settings);
+
+       gpiod_line_settings_set_bias(settings, GPIOD_LINE_BIAS_PULL_DOWN);
+       offset = 3;
+       gpiod_test_line_config_add_line_settings_or_fail(line_cfg, &offset, 1,
+                                                        settings);
+
+       gpiod_line_settings_set_bias(settings, GPIOD_LINE_BIAS_DISABLED);
+       offset = 4;
+       gpiod_test_line_config_add_line_settings_or_fail(line_cfg, &offset, 1,
+                                                        settings);
+
+       gpiod_line_settings_set_active_low(settings, true);
+       offset = 5;
+       gpiod_test_line_config_add_line_settings_or_fail(line_cfg, &offset, 1,
+                                                        settings);
+
+       gpiod_line_settings_set_edge_detection(settings,
+                                              GPIOD_LINE_EDGE_FALLING);
+       offset = 6;
+       gpiod_test_line_config_add_line_settings_or_fail(line_cfg, &offset, 1,
+                                                        settings);
+
+       gpiod_line_settings_set_event_clock(settings,
+                                           GPIOD_LINE_CLOCK_REALTIME);
+       offset = 7;
+       gpiod_test_line_config_add_line_settings_or_fail(line_cfg, &offset, 1,
+                                                        settings);
+
+       gpiod_line_settings_reset(settings);
+
+       gpiod_line_settings_set_direction(settings,
+                                         GPIOD_LINE_DIRECTION_OUTPUT);
+       gpiod_line_settings_set_drive(settings, GPIOD_LINE_DRIVE_OPEN_DRAIN);
+       offset = 8;
+       gpiod_test_line_config_add_line_settings_or_fail(line_cfg, &offset, 1,
+                                                        settings);
+
+       gpiod_line_settings_set_drive(settings, GPIOD_LINE_DRIVE_OPEN_SOURCE);
+       offset = 9;
+       gpiod_test_line_config_add_line_settings_or_fail(line_cfg, &offset, 1,
+                                                        settings);
+
+       request = gpiod_chip_request_lines(chip, NULL, line_cfg);
+       g_assert_null(request);
+       g_assert_cmpint(errno, ==, E2BIG);
+}
+
+GPIOD_TEST_CASE(null_settings)
+{
+       static const guint offsets[] = { 0, 1, 2, 3 };
+
+       g_autoptr(struct_gpiod_line_config) config = NULL;
+       g_autoptr(struct_gpiod_line_settings) settings = NULL;
+
+       config = gpiod_test_create_line_config_or_fail();
+
+       gpiod_test_line_config_add_line_settings_or_fail(config, offsets, 4,
+                                                        NULL);
+
+       settings = gpiod_test_line_config_get_line_settings_or_fail(config, 2);
+
+       g_assert_cmpint(gpiod_line_settings_get_drive(settings), ==,
+                       GPIOD_LINE_DIRECTION_AS_IS);
+}
+
+GPIOD_TEST_CASE(null_and_0_offsets)
+{
+       static const guint offsets[] = { 0, 1, 2, 3 };
+
+       g_autoptr(struct_gpiod_line_config) config = NULL;
+       g_autoptr(struct_gpiod_line_settings) settings = NULL;
+       gint ret;
+
+       config = gpiod_test_create_line_config_or_fail();
+       settings = gpiod_test_create_line_settings_or_fail();
+
+       ret = gpiod_line_config_add_line_settings(config, NULL, 4, settings);
+       g_assert_cmpint(ret, ==, -1);
+       gpiod_test_expect_errno(EINVAL);
+
+       ret = gpiod_line_config_add_line_settings(config, offsets, 0, settings);
+       g_assert_cmpint(ret, ==, -1);
+       gpiod_test_expect_errno(EINVAL);
+}
+
+GPIOD_TEST_CASE(reset_config)
+{
+       static const guint offsets[] = { 0, 1, 2, 3 };
+
+       g_autoptr(struct_gpiod_line_settings) settings = NULL;
+       g_autoptr(struct_gpiod_line_settings) retrieved0 = NULL;
+       g_autoptr(struct_gpiod_line_settings) retrieved1 = NULL;
+       g_autoptr(struct_gpiod_line_config) config = NULL;
+
+       settings = gpiod_test_create_line_settings_or_fail();
+       config = gpiod_test_create_line_config_or_fail();
+
+       gpiod_line_settings_set_direction(settings, GPIOD_LINE_DIRECTION_INPUT);
+       gpiod_line_settings_set_bias(settings, GPIOD_LINE_BIAS_PULL_DOWN);
+       gpiod_test_line_config_add_line_settings_or_fail(config, offsets, 4,
+                                                        settings);
+
+       retrieved0 = gpiod_test_line_config_get_line_settings_or_fail(config,
+                                                                     2);
+
+       g_assert_cmpint(gpiod_line_settings_get_direction(retrieved0), ==,
+                       GPIOD_LINE_DIRECTION_INPUT);
+       g_assert_cmpint(gpiod_line_settings_get_bias(retrieved0), ==,
+                       GPIOD_LINE_BIAS_PULL_DOWN);
+
+       gpiod_line_config_reset(config);
+
+       retrieved1 = gpiod_line_config_get_line_settings(config, 2);
+       g_assert_null(retrieved1);
+}
+
+GPIOD_TEST_CASE(get_offsets)
+{
+       g_autoptr(struct_gpiod_line_settings) settings = NULL;
+       g_autoptr(struct_gpiod_line_config) config = NULL;
+       guint offsets[8], offsets_in[4];
+       size_t num_offsets;
+
+       settings = gpiod_test_create_line_settings_or_fail();
+       config = gpiod_test_create_line_config_or_fail();
+
+       gpiod_line_settings_set_direction(settings, GPIOD_LINE_DIRECTION_INPUT);
+       gpiod_line_settings_set_bias(settings, GPIOD_LINE_BIAS_PULL_DOWN);
+       offsets[0] = 2;
+       offsets[1] = 4;
+       gpiod_test_line_config_add_line_settings_or_fail(config, offsets, 2,
+                                                        settings);
+
+       gpiod_line_settings_set_edge_detection(settings, GPIOD_LINE_EDGE_BOTH);
+       offsets[0] = 6;
+       offsets[1] = 7;
+       gpiod_test_line_config_add_line_settings_or_fail(config, offsets, 2,
+                                                        settings);
+
+       num_offsets = gpiod_line_config_get_configured_offsets(config,
+                                                              offsets_in, 4);
+       g_assert_cmpuint(num_offsets, ==, 4);
+       g_assert_cmpuint(offsets_in[0], ==, 2);
+       g_assert_cmpuint(offsets_in[1], ==, 4);
+       g_assert_cmpuint(offsets_in[2], ==, 6);
+       g_assert_cmpuint(offsets_in[3], ==, 7);
+}
+
+GPIOD_TEST_CASE(get_0_offsets)
+{
+       g_autoptr(struct_gpiod_line_config) config = NULL;
+       size_t num_offsets;
+       guint offsets[3];
+
+       config = gpiod_test_create_line_config_or_fail();
+
+       num_offsets = gpiod_line_config_get_configured_offsets(config,
+                                                              offsets, 0);
+       g_assert_cmpuint(num_offsets, ==, 0);
+}
+
+GPIOD_TEST_CASE(get_null_offsets)
+{
+       g_autoptr(struct_gpiod_line_config) config = NULL;
+       size_t num_offsets;
+
+       config = gpiod_test_create_line_config_or_fail();
+
+       num_offsets = gpiod_line_config_get_configured_offsets(config,
+                                                              NULL, 10);
+       g_assert_cmpuint(num_offsets, ==, 0);
+}
+
+GPIOD_TEST_CASE(get_less_offsets_than_configured)
+{
+       static const guint offsets[] = { 0, 1, 2, 3 };
+
+       g_autoptr(struct_gpiod_line_config) config = NULL;
+       size_t num_retrieved;
+       guint retrieved[3];
+
+       config = gpiod_test_create_line_config_or_fail();
+
+       gpiod_test_line_config_add_line_settings_or_fail(config, offsets, 4,
+                                                        NULL);
+
+       num_retrieved = gpiod_line_config_get_configured_offsets(config,
+                                                                retrieved, 3);
+       g_assert_cmpuint(num_retrieved, ==, 3);
+       g_assert_cmpuint(retrieved[0], ==, 0);
+       g_assert_cmpuint(retrieved[1], ==, 1);
+       g_assert_cmpuint(retrieved[2], ==, 2);
+}
+
+GPIOD_TEST_CASE(get_more_offsets_than_configured)
+{
+       static const guint offsets[] = { 0, 1, 2, 3 };
+
+       g_autoptr(struct_gpiod_line_config) config = NULL;
+       size_t num_retrieved;
+       guint retrieved[8];
+
+       config = gpiod_test_create_line_config_or_fail();
+
+       gpiod_test_line_config_add_line_settings_or_fail(config, offsets, 4,
+                                                        NULL);
+
+       num_retrieved = gpiod_line_config_get_configured_offsets(config,
+                                                                retrieved, 8);
+       g_assert_cmpuint(num_retrieved, ==, 4);
+       g_assert_cmpuint(retrieved[0], ==, 0);
+       g_assert_cmpuint(retrieved[1], ==, 1);
+       g_assert_cmpuint(retrieved[2], ==, 2);
+       g_assert_cmpuint(retrieved[3], ==, 3);
+}
+
+GPIOD_TEST_CASE(set_global_output_values)
+{
+       static const guint offsets[] = { 0, 1, 2, 3 };
+       static const enum gpiod_line_value values[] = {
+               GPIOD_LINE_VALUE_ACTIVE,
+               GPIOD_LINE_VALUE_INACTIVE,
+               GPIOD_LINE_VALUE_ACTIVE,
+               GPIOD_LINE_VALUE_INACTIVE,
+       };
+
+       g_autoptr(GPIOSimChip) sim = g_gpiosim_chip_new("num-lines", 4, NULL);
+       g_autoptr(struct_gpiod_line_settings) settings = NULL;
+       g_autoptr(struct_gpiod_line_config) config = NULL;
+       g_autoptr(struct_gpiod_line_request) request = NULL;
+       g_autoptr(struct_gpiod_chip) chip = NULL;
+
+       chip = gpiod_test_open_chip_or_fail(g_gpiosim_chip_get_dev_path(sim));
+       settings = gpiod_test_create_line_settings_or_fail();
+       config = gpiod_test_create_line_config_or_fail();
+
+       gpiod_line_settings_set_direction(settings,
+                                         GPIOD_LINE_DIRECTION_OUTPUT);
+       gpiod_test_line_config_add_line_settings_or_fail(config, offsets, 4,
+                                                        settings);
+       gpiod_test_line_config_set_output_values_or_fail(config, values, 4);
+
+       request = gpiod_test_chip_request_lines_or_fail(chip, NULL, config);
+
+       g_assert_cmpint(g_gpiosim_chip_get_value(sim, 0), ==,
+                       G_GPIOSIM_VALUE_ACTIVE);
+       g_assert_cmpint(g_gpiosim_chip_get_value(sim, 1), ==,
+                       G_GPIOSIM_VALUE_INACTIVE);
+       g_assert_cmpint(g_gpiosim_chip_get_value(sim, 2), ==,
+                       G_GPIOSIM_VALUE_ACTIVE);
+       g_assert_cmpint(g_gpiosim_chip_get_value(sim, 3), ==,
+                       G_GPIOSIM_VALUE_INACTIVE);
+}
+
+GPIOD_TEST_CASE(read_back_global_output_values)
+{
+       static const guint offsets[] = { 0, 1, 2, 3 };
+       static const enum gpiod_line_value values[] = {
+               GPIOD_LINE_VALUE_ACTIVE,
+               GPIOD_LINE_VALUE_INACTIVE,
+               GPIOD_LINE_VALUE_ACTIVE,
+               GPIOD_LINE_VALUE_INACTIVE,
+       };
+
+       g_autoptr(struct_gpiod_line_settings) settings = NULL;
+       g_autoptr(struct_gpiod_line_settings) retrieved = NULL;
+       g_autoptr(struct_gpiod_line_config) config = NULL;
+
+       settings = gpiod_test_create_line_settings_or_fail();
+       config = gpiod_test_create_line_config_or_fail();
+
+       gpiod_line_settings_set_direction(settings,
+                                         GPIOD_LINE_DIRECTION_OUTPUT);
+       gpiod_line_settings_set_output_value(settings, GPIOD_LINE_VALUE_ACTIVE);
+       gpiod_test_line_config_add_line_settings_or_fail(config, offsets, 4,
+                                                        settings);
+       gpiod_test_line_config_set_output_values_or_fail(config, values, 4);
+
+       retrieved = gpiod_test_line_config_get_line_settings_or_fail(config, 1);
+       g_assert_cmpint(gpiod_line_settings_get_output_value(retrieved), ==,
+                       GPIOD_LINE_VALUE_INACTIVE);
+}
+
+GPIOD_TEST_CASE(set_output_values_invalid_value)
+{
+       static const enum gpiod_line_value values[] = {
+               GPIOD_LINE_VALUE_ACTIVE,
+               GPIOD_LINE_VALUE_INACTIVE,
+               999,
+               GPIOD_LINE_VALUE_INACTIVE,
+       };
+
+       g_autoptr(struct_gpiod_line_config) config = NULL;
+
+       config = gpiod_test_create_line_config_or_fail();
+
+       g_assert_cmpint(gpiod_line_config_set_output_values(config, values, 4),
+                       ==, -1);
+       gpiod_test_expect_errno(EINVAL);
+}
+
+GPIOD_TEST_CASE(set_output_values_bad_args)
+{
+       static const enum gpiod_line_value values[] = {
+               GPIOD_LINE_VALUE_ACTIVE,
+               GPIOD_LINE_VALUE_INACTIVE,
+               GPIOD_LINE_VALUE_ACTIVE,
+               GPIOD_LINE_VALUE_INACTIVE,
+       };
+
+       g_autoptr(struct_gpiod_line_config) config = NULL;
+       gint ret;
+
+       config = gpiod_test_create_line_config_or_fail();
+
+       ret = gpiod_line_config_set_output_values(config, NULL, 4);
+       g_assert_cmpint(ret, ==, -1);
+       gpiod_test_expect_errno(EINVAL);
+
+       ret = gpiod_line_config_set_output_values(config, values, 0);
+       g_assert_cmpint(ret, ==, -1);
+       gpiod_test_expect_errno(EINVAL);
+}
+
+GPIOD_TEST_CASE(set_output_values_too_many_values)
+{
+       static const gsize num_values = 65;
+
+       g_autoptr(struct_gpiod_line_config) config = NULL;
+       g_autofree enum gpiod_line_value *values = NULL;
+       gint ret;
+       gsize i;
+
+       config = gpiod_test_create_line_config_or_fail();
+       values = g_malloc0(sizeof(*values) * num_values);
+
+       for (i = 0; i < num_values; i++)
+               values[i] = GPIOD_LINE_VALUE_ACTIVE;
+
+       ret = gpiod_line_config_set_output_values(config, values, num_values);
+       g_assert_cmpint(ret, ==, -1);
+       gpiod_test_expect_errno(EINVAL);
+}
+
+GPIOD_TEST_CASE(get_num_configured_offsets)
+{
+       static const guint offsets[] = { 0, 1, 2, 3 };
+
+       g_autoptr(struct_gpiod_line_config) config = NULL;
+       g_autoptr(struct_gpiod_line_settings) settings = NULL;
+
+       settings = gpiod_test_create_line_settings_or_fail();
+       config = gpiod_test_create_line_config_or_fail();
+
+       gpiod_test_line_config_add_line_settings_or_fail(config, offsets, 4,
+                                                        settings);
+
+       g_assert_cmpuint(gpiod_line_config_get_num_configured_offsets(config),
+                        ==, 4);
+}
+
+GPIOD_TEST_CASE(handle_duplicate_offsets)
+{
+       static const guint offsets[] = { 0, 2, 2, 3 };
+
+       g_autoptr(struct_gpiod_line_config) config = NULL;
+       size_t num_retrieved;
+       guint retrieved[3];
+
+       config = gpiod_test_create_line_config_or_fail();
+
+       gpiod_test_line_config_add_line_settings_or_fail(config, offsets, 4,
+                                                        NULL);
+
+       g_assert_cmpuint(gpiod_line_config_get_num_configured_offsets(config),
+                        ==, 3);
+       num_retrieved = gpiod_line_config_get_configured_offsets(config,
+                                                                retrieved, 3);
+       g_assert_cmpuint(num_retrieved, ==, 3);
+       g_assert_cmpuint(retrieved[0], ==, 0);
+       g_assert_cmpuint(retrieved[1], ==, 2);
+       g_assert_cmpuint(retrieved[2], ==, 3);
+}
diff --git a/tests/tests-line-info.c b/tests/tests-line-info.c
new file mode 100644 (file)
index 0000000..cf2c650
--- /dev/null
@@ -0,0 +1,407 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+// SPDX-FileCopyrightText: 2017-2021 Bartosz Golaszewski <bartekgola@gmail.com>
+
+#include <errno.h>
+#include <glib.h>
+#include <gpiod.h>
+
+#include "gpiod-test.h"
+#include "gpiod-test-helpers.h"
+#include "gpiod-test-sim.h"
+
+#define GPIOD_TEST_GROUP "line-info"
+
+GPIOD_TEST_CASE(get_line_info_good)
+{
+       g_autoptr(GPIOSimChip) sim = g_gpiosim_chip_new("num-lines", 8, NULL);
+       g_autoptr(struct_gpiod_chip) chip = NULL;
+       g_autoptr(struct_gpiod_line_info) info = NULL;
+
+       chip = gpiod_test_open_chip_or_fail(g_gpiosim_chip_get_dev_path(sim));
+
+       info = gpiod_test_chip_get_line_info_or_fail(chip, 3);
+       g_assert_cmpuint(gpiod_line_info_get_offset(info), ==, 3);
+}
+
+GPIOD_TEST_CASE(get_line_info_offset_out_of_range)
+{
+       g_autoptr(GPIOSimChip) sim = g_gpiosim_chip_new("num-lines", 8, NULL);
+       g_autoptr(struct_gpiod_chip) chip = NULL;
+       g_autoptr(struct_gpiod_line_info) info = NULL;
+
+       chip = gpiod_test_open_chip_or_fail(g_gpiosim_chip_get_dev_path(sim));
+
+       info = gpiod_chip_get_line_info(chip, 8);
+       g_assert_null(info);
+       gpiod_test_expect_errno(EINVAL);
+}
+
+GPIOD_TEST_CASE(line_info_basic_properties)
+{
+       static const GPIOSimLineName names[] = {
+               { .offset = 1, .name = "foo", },
+               { .offset = 2, .name = "bar", },
+               { .offset = 4, .name = "baz", },
+               { .offset = 5, .name = "xyz", },
+               { }
+       };
+
+       static const GPIOSimHog hogs[] = {
+               {
+                       .offset = 3,
+                       .name = "hog3",
+                       .direction = G_GPIOSIM_DIRECTION_OUTPUT_HIGH,
+               },
+               {
+                       .offset = 4,
+                       .name = "hog4",
+                       .direction = G_GPIOSIM_DIRECTION_OUTPUT_LOW,
+               },
+               { }
+       };
+
+       g_autoptr(GPIOSimChip) sim = NULL;
+       g_autoptr(struct_gpiod_chip) chip = NULL;
+       g_autoptr(struct_gpiod_line_info) info4 = NULL;
+       g_autoptr(struct_gpiod_line_info) info6 = NULL;
+       g_autoptr(GVariant) vnames = gpiod_test_package_line_names(names);
+       g_autoptr(GVariant) vhogs = gpiod_test_package_hogs(hogs);
+
+       sim = g_gpiosim_chip_new(
+                       "num-lines", 8,
+                       "line-names", vnames,
+                       "hogs", vhogs,
+                       NULL);
+
+       chip = gpiod_test_open_chip_or_fail(g_gpiosim_chip_get_dev_path(sim));
+       info4 = gpiod_test_chip_get_line_info_or_fail(chip, 4);
+       info6 = gpiod_test_chip_get_line_info_or_fail(chip, 6);
+
+       g_assert_cmpuint(gpiod_line_info_get_offset(info4), ==, 4);
+       g_assert_cmpstr(gpiod_line_info_get_name(info4), ==, "baz");
+       g_assert_cmpstr(gpiod_line_info_get_consumer(info4), ==, "hog4");
+       g_assert_true(gpiod_line_info_is_used(info4));
+       g_assert_cmpint(gpiod_line_info_get_direction(info4), ==,
+                       GPIOD_LINE_DIRECTION_OUTPUT);
+       g_assert_cmpint(gpiod_line_info_get_edge_detection(info4), ==,
+                       GPIOD_LINE_EDGE_NONE);
+       g_assert_false(gpiod_line_info_is_active_low(info4));
+       g_assert_cmpint(gpiod_line_info_get_bias(info4), ==,
+                       GPIOD_LINE_BIAS_UNKNOWN);
+       g_assert_cmpint(gpiod_line_info_get_drive(info4), ==,
+                       GPIOD_LINE_DRIVE_PUSH_PULL);
+       g_assert_cmpint(gpiod_line_info_get_event_clock(info4), ==,
+                       GPIOD_LINE_CLOCK_MONOTONIC);
+       g_assert_false(gpiod_line_info_is_debounced(info4));
+       g_assert_cmpuint(gpiod_line_info_get_debounce_period_us(info4), ==, 0);
+}
+
+GPIOD_TEST_CASE(copy_line_info)
+{
+       g_autoptr(GPIOSimChip) sim = g_gpiosim_chip_new("num-lines", 8, NULL);
+       g_autoptr(struct_gpiod_chip) chip = NULL;
+       g_autoptr(struct_gpiod_line_info) info = NULL;
+       g_autoptr(struct_gpiod_line_info) copy = NULL;
+
+       chip = gpiod_test_open_chip_or_fail(g_gpiosim_chip_get_dev_path(sim));
+       info = gpiod_test_chip_get_line_info_or_fail(chip, 3);
+
+       copy = gpiod_line_info_copy(info);
+       g_assert_nonnull(copy);
+       g_assert_true(info != copy);
+       g_assert_cmpuint(gpiod_line_info_get_offset(info), ==,
+                        gpiod_line_info_get_offset(copy));
+}
+
+GPIOD_TEST_CASE(direction_settings)
+{
+       g_autoptr(GPIOSimChip) sim = g_gpiosim_chip_new("num-lines", 8, NULL);
+       g_autoptr(struct_gpiod_chip) chip = NULL;
+       g_autoptr(struct_gpiod_line_config) line_cfg = NULL;
+       g_autoptr(struct_gpiod_line_settings) settings = NULL;
+       g_autoptr(struct_gpiod_line_request) request = NULL;
+       g_autoptr(struct_gpiod_line_info) info0 = NULL;
+       g_autoptr(struct_gpiod_line_info) info1 = NULL;
+       g_autoptr(struct_gpiod_line_info) info2 = NULL;
+       guint offset;
+
+       chip = gpiod_test_open_chip_or_fail(g_gpiosim_chip_get_dev_path(sim));
+       settings = gpiod_test_create_line_settings_or_fail();
+       line_cfg = gpiod_test_create_line_config_or_fail();
+
+       gpiod_line_settings_set_direction(settings,
+                                         GPIOD_LINE_DIRECTION_OUTPUT);
+       offset = 0;
+       gpiod_test_line_config_add_line_settings_or_fail(line_cfg, &offset, 1,
+                                                        settings);
+       gpiod_line_settings_set_direction(settings, GPIOD_LINE_DIRECTION_INPUT);
+       offset = 1;
+       gpiod_test_line_config_add_line_settings_or_fail(line_cfg, &offset, 1,
+                                                        settings);
+       gpiod_line_settings_set_direction(settings, GPIOD_LINE_DIRECTION_AS_IS);
+       offset = 2;
+       gpiod_test_line_config_add_line_settings_or_fail(line_cfg, &offset, 1,
+                                                        settings);
+
+       request = gpiod_test_chip_request_lines_or_fail(chip, NULL, line_cfg);
+       info0 = gpiod_test_chip_get_line_info_or_fail(chip, 0);
+       info1 = gpiod_test_chip_get_line_info_or_fail(chip, 1);
+       info2 = gpiod_test_chip_get_line_info_or_fail(chip, 2);
+
+       g_assert_cmpint(gpiod_line_info_get_direction(info0), ==,
+                       GPIOD_LINE_DIRECTION_OUTPUT);
+       g_assert_cmpint(gpiod_line_info_get_direction(info1), ==,
+                       GPIOD_LINE_DIRECTION_INPUT);
+       g_assert_cmpint(gpiod_line_info_get_direction(info2), ==,
+                       GPIOD_LINE_DIRECTION_INPUT);
+}
+
+GPIOD_TEST_CASE(active_high)
+{
+       static const guint offset = 5;
+
+       g_autoptr(GPIOSimChip) sim = g_gpiosim_chip_new("num-lines", 8, NULL);
+       g_autoptr(struct_gpiod_chip) chip = NULL;
+       g_autoptr(struct_gpiod_line_config) line_cfg = NULL;
+       g_autoptr(struct_gpiod_line_settings) settings = NULL;
+       g_autoptr(struct_gpiod_line_request) request = NULL;
+       g_autoptr(struct_gpiod_line_info) info = NULL;
+
+       chip = gpiod_test_open_chip_or_fail(g_gpiosim_chip_get_dev_path(sim));
+       settings = gpiod_test_create_line_settings_or_fail();
+       line_cfg = gpiod_test_create_line_config_or_fail();
+
+       gpiod_line_settings_set_active_low(settings, true);
+       gpiod_test_line_config_add_line_settings_or_fail(line_cfg, &offset,
+                                                        1, settings);
+
+       request = gpiod_test_chip_request_lines_or_fail(chip, NULL, line_cfg);
+       info = gpiod_test_chip_get_line_info_or_fail(chip, 5);
+
+       g_assert_true(gpiod_line_info_is_active_low(info));
+}
+
+GPIOD_TEST_CASE(edge_detection_settings)
+{
+       g_autoptr(GPIOSimChip) sim = g_gpiosim_chip_new("num-lines", 8, NULL);
+       g_autoptr(struct_gpiod_chip) chip = NULL;
+       g_autoptr(struct_gpiod_line_config) line_cfg = NULL;
+       g_autoptr(struct_gpiod_line_request) request = NULL;
+       g_autoptr(struct_gpiod_line_settings) settings = NULL;
+       g_autoptr(struct_gpiod_line_info) info0 = NULL;
+       g_autoptr(struct_gpiod_line_info) info1 = NULL;
+       g_autoptr(struct_gpiod_line_info) info2 = NULL;
+       g_autoptr(struct_gpiod_line_info) info3 = NULL;
+       guint offset;
+
+       chip = gpiod_test_open_chip_or_fail(g_gpiosim_chip_get_dev_path(sim));
+       line_cfg = gpiod_test_create_line_config_or_fail();
+       settings = gpiod_test_create_line_settings_or_fail();
+
+       gpiod_line_settings_set_edge_detection(settings, GPIOD_LINE_EDGE_NONE);
+       offset = 0;
+       gpiod_test_line_config_add_line_settings_or_fail(line_cfg, &offset, 1,
+                                                        settings);
+       gpiod_line_settings_set_edge_detection(settings,
+                                              GPIOD_LINE_EDGE_RISING);
+       offset = 1;
+       gpiod_test_line_config_add_line_settings_or_fail(line_cfg, &offset, 1,
+                                                        settings);
+       gpiod_line_settings_set_edge_detection(settings,
+                                              GPIOD_LINE_EDGE_FALLING);
+       offset = 2;
+       gpiod_test_line_config_add_line_settings_or_fail(line_cfg, &offset, 1,
+                                                        settings);
+       gpiod_line_settings_set_edge_detection(settings, GPIOD_LINE_EDGE_BOTH);
+       offset = 3;
+       gpiod_test_line_config_add_line_settings_or_fail(line_cfg, &offset, 1,
+                                                        settings);
+
+       request = gpiod_test_chip_request_lines_or_fail(chip, NULL, line_cfg);
+       info0 = gpiod_test_chip_get_line_info_or_fail(chip, 0);
+       info1 = gpiod_test_chip_get_line_info_or_fail(chip, 1);
+       info2 = gpiod_test_chip_get_line_info_or_fail(chip, 2);
+       info3 = gpiod_test_chip_get_line_info_or_fail(chip, 3);
+
+       g_assert_cmpint(gpiod_line_info_get_edge_detection(info0), ==,
+                       GPIOD_LINE_EDGE_NONE);
+       g_assert_cmpint(gpiod_line_info_get_edge_detection(info1), ==,
+                       GPIOD_LINE_EDGE_RISING);
+       g_assert_cmpint(gpiod_line_info_get_edge_detection(info2), ==,
+                       GPIOD_LINE_EDGE_FALLING);
+       g_assert_cmpint(gpiod_line_info_get_edge_detection(info3), ==,
+                       GPIOD_LINE_EDGE_BOTH);
+}
+
+GPIOD_TEST_CASE(bias_settings)
+{
+       g_autoptr(GPIOSimChip) sim = g_gpiosim_chip_new("num-lines", 8, NULL);
+       g_autoptr(struct_gpiod_chip) chip = NULL;
+       g_autoptr(struct_gpiod_line_config) line_cfg = NULL;
+       g_autoptr(struct_gpiod_line_settings) settings = NULL;
+       g_autoptr(struct_gpiod_line_request) request = NULL;
+       g_autoptr(struct_gpiod_line_info) info0 = NULL;
+       g_autoptr(struct_gpiod_line_info) info1 = NULL;
+       g_autoptr(struct_gpiod_line_info) info2 = NULL;
+       g_autoptr(struct_gpiod_line_info) info3 = NULL;
+       guint offset;
+
+       chip = gpiod_test_open_chip_or_fail(g_gpiosim_chip_get_dev_path(sim));
+       settings = gpiod_test_create_line_settings_or_fail();
+       line_cfg = gpiod_test_create_line_config_or_fail();
+
+       gpiod_line_settings_set_direction(settings,
+                                         GPIOD_LINE_DIRECTION_OUTPUT);
+       offset = 0;
+       gpiod_test_line_config_add_line_settings_or_fail(line_cfg, &offset, 1,
+                                                        settings);
+       gpiod_line_settings_set_bias(settings, GPIOD_LINE_BIAS_DISABLED);
+       offset = 1;
+       gpiod_test_line_config_add_line_settings_or_fail(line_cfg, &offset, 1,
+                                                        settings);
+       gpiod_line_settings_set_bias(settings, GPIOD_LINE_BIAS_PULL_DOWN);
+       offset = 2;
+       gpiod_test_line_config_add_line_settings_or_fail(line_cfg, &offset, 1,
+                                                        settings);
+       gpiod_line_settings_set_bias(settings, GPIOD_LINE_BIAS_PULL_UP);
+       offset = 3;
+       gpiod_test_line_config_add_line_settings_or_fail(line_cfg, &offset, 1,
+                                                        settings);
+
+       request = gpiod_test_chip_request_lines_or_fail(chip, NULL, line_cfg);
+       info0 = gpiod_test_chip_get_line_info_or_fail(chip, 0);
+       info1 = gpiod_test_chip_get_line_info_or_fail(chip, 1);
+       info2 = gpiod_test_chip_get_line_info_or_fail(chip, 2);
+       info3 = gpiod_test_chip_get_line_info_or_fail(chip, 3);
+
+       g_assert_cmpint(gpiod_line_info_get_bias(info0), ==,
+                       GPIOD_LINE_BIAS_UNKNOWN);
+       g_assert_cmpint(gpiod_line_info_get_bias(info1), ==,
+                       GPIOD_LINE_BIAS_DISABLED);
+       g_assert_cmpint(gpiod_line_info_get_bias(info2), ==,
+                       GPIOD_LINE_BIAS_PULL_DOWN);
+       g_assert_cmpint(gpiod_line_info_get_bias(info3), ==,
+                       GPIOD_LINE_BIAS_PULL_UP);
+}
+
+GPIOD_TEST_CASE(drive_settings)
+{
+       g_autoptr(GPIOSimChip) sim = g_gpiosim_chip_new("num-lines", 8, NULL);
+       g_autoptr(struct_gpiod_chip) chip = NULL;
+       g_autoptr(struct_gpiod_line_config) line_cfg = NULL;
+       g_autoptr(struct_gpiod_line_settings) settings = NULL;
+       g_autoptr(struct_gpiod_line_request) request = NULL;
+       g_autoptr(struct_gpiod_line_info) info0 = NULL;
+       g_autoptr(struct_gpiod_line_info) info1 = NULL;
+       g_autoptr(struct_gpiod_line_info) info2 = NULL;
+       guint offset;
+
+       chip = gpiod_test_open_chip_or_fail(g_gpiosim_chip_get_dev_path(sim));
+       line_cfg = gpiod_test_create_line_config_or_fail();
+       settings = gpiod_test_create_line_settings_or_fail();
+
+       gpiod_line_settings_set_direction(settings,
+                                         GPIOD_LINE_DIRECTION_OUTPUT);
+       offset = 0;
+       gpiod_test_line_config_add_line_settings_or_fail(line_cfg, &offset, 1,
+                                                        settings);
+       gpiod_line_settings_set_drive(settings, GPIOD_LINE_DRIVE_OPEN_DRAIN);
+       offset = 1;
+       gpiod_test_line_config_add_line_settings_or_fail(line_cfg, &offset, 1,
+                                                        settings);
+       gpiod_line_settings_set_drive(settings, GPIOD_LINE_DRIVE_OPEN_SOURCE);
+       offset = 2;
+       gpiod_test_line_config_add_line_settings_or_fail(line_cfg, &offset, 1,
+                                                        settings);
+
+       request = gpiod_test_chip_request_lines_or_fail(chip, NULL, line_cfg);
+       info0 = gpiod_test_chip_get_line_info_or_fail(chip, 0);
+       info1 = gpiod_test_chip_get_line_info_or_fail(chip, 1);
+       info2 = gpiod_test_chip_get_line_info_or_fail(chip, 2);
+
+       g_assert_cmpint(gpiod_line_info_get_drive(info0), ==,
+                       GPIOD_LINE_DRIVE_PUSH_PULL);
+       g_assert_cmpint(gpiod_line_info_get_drive(info1), ==,
+                       GPIOD_LINE_DRIVE_OPEN_DRAIN);
+       g_assert_cmpint(gpiod_line_info_get_drive(info2), ==,
+                       GPIOD_LINE_DRIVE_OPEN_SOURCE);
+}
+
+GPIOD_TEST_CASE(debounce_period)
+{
+       static const guint offset = 5;
+
+       g_autoptr(GPIOSimChip) sim = g_gpiosim_chip_new("num-lines", 8, NULL);
+       g_autoptr(struct_gpiod_chip) chip = NULL;
+       g_autoptr(struct_gpiod_line_config) line_cfg = NULL;
+       g_autoptr(struct_gpiod_line_settings) settings = NULL;
+       g_autoptr(struct_gpiod_line_request) request = NULL;
+       g_autoptr(struct_gpiod_line_info) info = NULL;
+
+       chip = gpiod_test_open_chip_or_fail(g_gpiosim_chip_get_dev_path(sim));
+       line_cfg = gpiod_test_create_line_config_or_fail();
+       settings = gpiod_test_create_line_settings_or_fail();
+
+       gpiod_line_settings_set_edge_detection(settings, GPIOD_LINE_EDGE_BOTH);
+       gpiod_line_settings_set_debounce_period_us(settings, 1000);
+
+       gpiod_test_line_config_add_line_settings_or_fail(line_cfg, &offset, 1,
+                                                        settings);
+
+       request = gpiod_test_chip_request_lines_or_fail(chip, NULL, line_cfg);
+       info = gpiod_test_chip_get_line_info_or_fail(chip, 5);
+
+       g_assert_cmpuint(gpiod_line_info_get_debounce_period_us(info), ==,
+                        1000);
+}
+
+GPIOD_TEST_CASE(event_clock)
+{
+       g_autoptr(GPIOSimChip) sim = g_gpiosim_chip_new("num-lines", 8, NULL);
+       g_autoptr(struct_gpiod_chip) chip = NULL;
+       g_autoptr(struct_gpiod_line_config) line_cfg = NULL;
+       g_autoptr(struct_gpiod_line_settings) settings = NULL;
+       g_autoptr(struct_gpiod_line_request) request = NULL;
+       g_autoptr(struct_gpiod_line_info) info0 = NULL;
+       g_autoptr(struct_gpiod_line_info) info1 = NULL;
+       g_autoptr(struct_gpiod_line_info) info2 = NULL;
+       guint offset;
+
+       chip = gpiod_test_open_chip_or_fail(g_gpiosim_chip_get_dev_path(sim));
+       line_cfg = gpiod_test_create_line_config_or_fail();
+       settings = gpiod_test_create_line_settings_or_fail();
+
+       offset = 0;
+       gpiod_test_line_config_add_line_settings_or_fail(line_cfg, &offset, 1,
+                                                        settings);
+       gpiod_line_settings_set_event_clock(settings,
+                                           GPIOD_LINE_CLOCK_REALTIME);
+       offset = 1;
+       gpiod_test_line_config_add_line_settings_or_fail(line_cfg, &offset, 1,
+                                                        settings);
+
+       gpiod_line_settings_set_event_clock(settings,
+                                           GPIOD_LINE_CLOCK_HTE);
+       offset = 2;
+       gpiod_test_line_config_add_line_settings_or_fail(line_cfg, &offset, 1,
+                                                        settings);
+
+       request = gpiod_chip_request_lines(chip, NULL, line_cfg);
+       if (!request && errno == EOPNOTSUPP) {
+               g_test_skip("HTE support not available");
+               return;
+       }
+
+       gpiod_test_return_if_failed();
+
+       info0 = gpiod_test_chip_get_line_info_or_fail(chip, 0);
+       info1 = gpiod_test_chip_get_line_info_or_fail(chip, 1);
+       info2 = gpiod_test_chip_get_line_info_or_fail(chip, 2);
+
+       g_assert_cmpint(gpiod_line_info_get_event_clock(info0), ==,
+                       GPIOD_LINE_CLOCK_MONOTONIC);
+       g_assert_cmpint(gpiod_line_info_get_event_clock(info1), ==,
+                       GPIOD_LINE_CLOCK_REALTIME);
+       g_assert_cmpint(gpiod_line_info_get_event_clock(info2), ==,
+                       GPIOD_LINE_CLOCK_HTE);
+}
diff --git a/tests/tests-line-request.c b/tests/tests-line-request.c
new file mode 100644 (file)
index 0000000..7bba078
--- /dev/null
@@ -0,0 +1,701 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+// SPDX-FileCopyrightText: 2017-2021 Bartosz Golaszewski <bartekgola@gmail.com>
+
+#include <glib.h>
+#include <gpiod.h>
+
+#include "gpiod-test.h"
+#include "gpiod-test-helpers.h"
+#include "gpiod-test-sim.h"
+
+#define GPIOD_TEST_GROUP "line-request"
+
+GPIOD_TEST_CASE(request_fails_with_no_offsets)
+{
+       g_autoptr(GPIOSimChip) sim = g_gpiosim_chip_new("num-lines", 4, NULL);
+       g_autoptr(struct_gpiod_chip) chip = NULL;
+       g_autoptr(struct_gpiod_line_config) line_cfg = NULL;
+       g_autoptr(struct_gpiod_line_request) request = NULL;
+
+       line_cfg = gpiod_test_create_line_config_or_fail();
+
+       chip = gpiod_test_open_chip_or_fail(g_gpiosim_chip_get_dev_path(sim));
+
+       request = gpiod_chip_request_lines(chip, NULL, line_cfg);
+       g_assert_null(request);
+       gpiod_test_expect_errno(EINVAL);
+}
+
+GPIOD_TEST_CASE(request_fails_with_no_line_config)
+{
+       g_autoptr(GPIOSimChip) sim = g_gpiosim_chip_new("num-lines", 4, NULL);
+       g_autoptr(struct_gpiod_chip) chip = NULL;
+       g_autoptr(struct_gpiod_line_request) request = NULL;
+
+       chip = gpiod_test_open_chip_or_fail(g_gpiosim_chip_get_dev_path(sim));
+
+       request = gpiod_chip_request_lines(chip, NULL, NULL);
+       g_assert_null(request);
+       gpiod_test_expect_errno(EINVAL);
+}
+
+GPIOD_TEST_CASE(request_fails_with_offset_out_of_bounds)
+{
+       static const guint offsets[] = { 2, 6 };
+
+       g_autoptr(GPIOSimChip) sim = g_gpiosim_chip_new("num-lines", 4, NULL);
+       g_autoptr(struct_gpiod_chip) chip = NULL;
+       g_autoptr(struct_gpiod_line_config) line_cfg = NULL;
+       g_autoptr(struct_gpiod_line_request) request = NULL;
+
+       chip = gpiod_test_open_chip_or_fail(g_gpiosim_chip_get_dev_path(sim));
+       line_cfg = gpiod_test_create_line_config_or_fail();
+
+       gpiod_test_line_config_add_line_settings_or_fail(line_cfg, offsets, 2,
+                                                        NULL);
+
+       request = gpiod_chip_request_lines(chip, NULL, line_cfg);
+       g_assert_null(request);
+       gpiod_test_expect_errno(EINVAL);
+}
+
+GPIOD_TEST_CASE(set_consumer)
+{
+       static const guint offset = 2;
+       static const gchar *const consumer = "foobar";
+
+       g_autoptr(GPIOSimChip) sim = g_gpiosim_chip_new("num-lines", 4, NULL);
+       g_autoptr(struct_gpiod_chip) chip = NULL;
+       g_autoptr(struct_gpiod_request_config) req_cfg = NULL;
+       g_autoptr(struct_gpiod_line_config) line_cfg = NULL;
+       g_autoptr(struct_gpiod_line_request) request = NULL;
+       g_autoptr(struct_gpiod_line_info) info = NULL;
+
+       chip = gpiod_test_open_chip_or_fail(g_gpiosim_chip_get_dev_path(sim));
+       req_cfg = gpiod_test_create_request_config_or_fail();
+       line_cfg = gpiod_test_create_line_config_or_fail();
+
+       gpiod_request_config_set_consumer(req_cfg, consumer);
+
+       gpiod_test_line_config_add_line_settings_or_fail(line_cfg, &offset, 1,
+                                                        NULL);
+
+       request = gpiod_test_chip_request_lines_or_fail(chip,
+                                                       req_cfg, line_cfg);
+
+       info = gpiod_test_chip_get_line_info_or_fail(chip, offset);
+
+       g_assert_true(gpiod_line_info_is_used(info));
+       g_assert_cmpstr(gpiod_line_info_get_consumer(info), ==, consumer);
+}
+
+GPIOD_TEST_CASE(empty_consumer)
+{
+       static const guint offset = 2;
+
+       g_autoptr(GPIOSimChip) sim = g_gpiosim_chip_new("num-lines", 4, NULL);
+       g_autoptr(struct_gpiod_chip) chip = NULL;
+       g_autoptr(struct_gpiod_line_config) line_cfg = NULL;
+       g_autoptr(struct_gpiod_line_request) request = NULL;
+       g_autoptr(struct_gpiod_line_info) info = NULL;
+
+       chip = gpiod_test_open_chip_or_fail(g_gpiosim_chip_get_dev_path(sim));
+       line_cfg = gpiod_test_create_line_config_or_fail();
+
+       gpiod_test_line_config_add_line_settings_or_fail(line_cfg, &offset, 1,
+                                                        NULL);
+
+       request = gpiod_test_chip_request_lines_or_fail(chip, NULL, line_cfg);
+
+       info = gpiod_test_chip_get_line_info_or_fail(chip, offset);
+
+       g_assert_cmpstr(gpiod_line_info_get_consumer(info), ==, "?");
+}
+
+GPIOD_TEST_CASE(default_output_value)
+{
+       /*
+        * Have a hole in offsets on purpose - make sure it's not set by
+        * accident.
+        */
+       static const guint offsets[] = { 0, 1, 3, 4 };
+
+       g_autoptr(GPIOSimChip) sim = g_gpiosim_chip_new("num-lines", 8, NULL);
+       g_autoptr(struct_gpiod_chip) chip = NULL;
+       g_autoptr(struct_gpiod_line_settings) settings = NULL;
+       g_autoptr(struct_gpiod_line_config) line_cfg = NULL;
+       g_autoptr(struct_gpiod_line_request) request = NULL;
+       guint i;
+
+       chip = gpiod_test_open_chip_or_fail(g_gpiosim_chip_get_dev_path(sim));
+       settings = gpiod_test_create_line_settings_or_fail();
+       line_cfg = gpiod_test_create_line_config_or_fail();
+
+       gpiod_line_settings_set_direction(settings,
+                                         GPIOD_LINE_DIRECTION_OUTPUT);
+       gpiod_line_settings_set_output_value(settings, GPIOD_LINE_VALUE_ACTIVE);
+
+       gpiod_test_line_config_add_line_settings_or_fail(line_cfg, offsets, 4,
+                                                        settings);
+
+       g_gpiosim_chip_set_pull(sim, 2, G_GPIOSIM_PULL_DOWN);
+
+       request = gpiod_test_chip_request_lines_or_fail(chip, NULL, line_cfg);
+
+       for (i = 0; i < 4; i++)
+               g_assert_cmpint(g_gpiosim_chip_get_value(sim, offsets[i]), ==,
+                               G_GPIOSIM_VALUE_ACTIVE);
+
+       g_assert_cmpint(g_gpiosim_chip_get_value(sim, 2), ==,
+                       G_GPIOSIM_VALUE_INACTIVE);
+}
+
+GPIOD_TEST_CASE(read_all_values)
+{
+       static const guint offsets[] = { 0, 2, 4, 5, 7 };
+       static const gint pulls[] = { 0, 1, 0, 1, 1 };
+
+       g_autoptr(GPIOSimChip) sim = g_gpiosim_chip_new("num-lines", 8, NULL);
+       g_autoptr(struct_gpiod_chip) chip = NULL;
+       g_autoptr(struct_gpiod_line_settings) settings = NULL;
+       g_autoptr(struct_gpiod_line_config) line_cfg = NULL;
+       g_autoptr(struct_gpiod_line_request) request = NULL;
+       enum gpiod_line_value values[5];
+       gint ret;
+       guint i;
+
+       chip = gpiod_test_open_chip_or_fail(g_gpiosim_chip_get_dev_path(sim));
+       settings = gpiod_test_create_line_settings_or_fail();
+       line_cfg = gpiod_test_create_line_config_or_fail();
+
+       gpiod_line_settings_set_direction(settings, GPIOD_LINE_DIRECTION_INPUT);
+       gpiod_test_line_config_add_line_settings_or_fail(line_cfg, offsets, 5,
+                                                        settings);
+
+       request = gpiod_test_chip_request_lines_or_fail(chip, NULL, line_cfg);
+
+       for (i = 0; i < 5; i++)
+               g_gpiosim_chip_set_pull(sim, offsets[i],
+                                       pulls[i] ? G_GPIOSIM_PULL_UP :
+                                                  G_GPIOSIM_PULL_DOWN);
+
+       ret = gpiod_line_request_get_values(request, values);
+       g_assert_cmpint(ret, ==, 0);
+       gpiod_test_return_if_failed();
+
+       for (i = 0; i < 5; i++)
+               g_assert_cmpint(values[i], ==, pulls[i]);
+}
+
+GPIOD_TEST_CASE(request_multiple_values_but_read_one)
+{
+       static const guint offsets[] = { 0, 2, 4, 5, 7 };
+       static const gint pulls[] = { 0, 1, 0, 1, 1 };
+
+       g_autoptr(GPIOSimChip) sim = g_gpiosim_chip_new("num-lines", 8, NULL);
+       g_autoptr(struct_gpiod_chip) chip = NULL;
+       g_autoptr(struct_gpiod_line_settings) settings = NULL;
+       g_autoptr(struct_gpiod_line_config) line_cfg = NULL;
+       g_autoptr(struct_gpiod_line_request) request = NULL;
+       gint ret;
+       guint i;
+
+       chip = gpiod_test_open_chip_or_fail(g_gpiosim_chip_get_dev_path(sim));
+       settings = gpiod_test_create_line_settings_or_fail();
+       line_cfg = gpiod_test_create_line_config_or_fail();
+
+       gpiod_line_settings_set_direction(settings, GPIOD_LINE_DIRECTION_INPUT);
+       gpiod_test_line_config_add_line_settings_or_fail(line_cfg, offsets, 5,
+                                                        settings);
+
+       request = gpiod_test_chip_request_lines_or_fail(chip, NULL, line_cfg);
+
+       for (i = 0; i < 5; i++)
+               g_gpiosim_chip_set_pull(sim, offsets[i],
+                                       pulls[i] ? G_GPIOSIM_PULL_UP :
+                                                  G_GPIOSIM_PULL_DOWN);
+
+       ret = gpiod_line_request_get_value(request, 5);
+       g_assert_cmpint(ret, ==, 1);
+}
+
+GPIOD_TEST_CASE(set_all_values)
+{
+       static const guint offsets[] = { 0, 2, 4, 5, 6 };
+       static const enum gpiod_line_value values[] = {
+               GPIOD_LINE_VALUE_ACTIVE,
+               GPIOD_LINE_VALUE_INACTIVE,
+               GPIOD_LINE_VALUE_ACTIVE,
+               GPIOD_LINE_VALUE_ACTIVE,
+               GPIOD_LINE_VALUE_ACTIVE
+       };
+       static const GPIOSimValue sim_values[] = {
+               G_GPIOSIM_VALUE_ACTIVE,
+               G_GPIOSIM_VALUE_INACTIVE,
+               G_GPIOSIM_VALUE_ACTIVE,
+               G_GPIOSIM_VALUE_ACTIVE,
+               G_GPIOSIM_VALUE_ACTIVE
+       };
+
+       g_autoptr(GPIOSimChip) sim = g_gpiosim_chip_new("num-lines", 8, NULL);
+       g_autoptr(struct_gpiod_chip) chip = NULL;
+       g_autoptr(struct_gpiod_line_settings) settings = NULL;
+       g_autoptr(struct_gpiod_line_config) line_cfg = NULL;
+       g_autoptr(struct_gpiod_line_request) request = NULL;
+       gint ret;
+       guint i;
+
+       chip = gpiod_test_open_chip_or_fail(g_gpiosim_chip_get_dev_path(sim));
+       settings = gpiod_test_create_line_settings_or_fail();
+       line_cfg = gpiod_test_create_line_config_or_fail();
+
+       gpiod_line_settings_set_direction(settings,
+                                         GPIOD_LINE_DIRECTION_OUTPUT);
+       gpiod_test_line_config_add_line_settings_or_fail(line_cfg, offsets, 5,
+                                                        settings);
+
+       request = gpiod_test_chip_request_lines_or_fail(chip, NULL, line_cfg);
+
+       ret = gpiod_line_request_set_values(request, values);
+       g_assert_cmpint(ret, ==, 0);
+       gpiod_test_return_if_failed();
+
+       for (i = 0; i < 5; i++)
+               g_assert_cmpint(g_gpiosim_chip_get_value(sim, offsets[i]), ==,
+                               sim_values[i]);
+}
+
+GPIOD_TEST_CASE(set_values_subset_of_lines)
+{
+       static const guint offsets[] = { 0, 1, 2, 3 };
+       static const guint offsets_to_set[] = { 0, 1, 3 };
+       static const enum gpiod_line_value values[] = {
+               GPIOD_LINE_VALUE_ACTIVE,
+               GPIOD_LINE_VALUE_INACTIVE,
+               GPIOD_LINE_VALUE_ACTIVE
+       };
+
+       g_autoptr(GPIOSimChip) sim = g_gpiosim_chip_new("num-lines", 4, NULL);
+       g_autoptr(struct_gpiod_chip) chip = NULL;
+       g_autoptr(struct_gpiod_line_settings) settings = NULL;
+       g_autoptr(struct_gpiod_line_config) line_cfg = NULL;
+       g_autoptr(struct_gpiod_line_request) request = NULL;
+       gint ret;
+
+       chip = gpiod_test_open_chip_or_fail(g_gpiosim_chip_get_dev_path(sim));
+       settings = gpiod_test_create_line_settings_or_fail();
+       line_cfg = gpiod_test_create_line_config_or_fail();
+
+       gpiod_line_settings_set_direction(settings,
+                                         GPIOD_LINE_DIRECTION_OUTPUT);
+       gpiod_test_line_config_add_line_settings_or_fail(line_cfg, offsets, 4,
+                                                        settings);
+
+       request = gpiod_test_chip_request_lines_or_fail(chip, NULL, line_cfg);
+
+       ret = gpiod_line_request_set_values_subset(request, 3, offsets_to_set,
+                                                  values);
+       g_assert_cmpint(ret, ==, 0);
+       gpiod_test_return_if_failed();
+
+       g_assert_cmpint(g_gpiosim_chip_get_value(sim, 0), ==,
+                       G_GPIOSIM_VALUE_ACTIVE);
+       g_assert_cmpint(g_gpiosim_chip_get_value(sim, 1), ==,
+                       G_GPIOSIM_VALUE_INACTIVE);
+       g_assert_cmpint(g_gpiosim_chip_get_value(sim, 3), ==,
+                       G_GPIOSIM_VALUE_ACTIVE);
+}
+
+GPIOD_TEST_CASE(set_line_after_requesting)
+{
+       static const guint offsets[] = { 0, 1, 3, 4 };
+
+       g_autoptr(GPIOSimChip) sim = g_gpiosim_chip_new("num-lines", 8, NULL);
+       g_autoptr(struct_gpiod_chip) chip = NULL;
+       g_autoptr(struct_gpiod_line_settings) settings = NULL;
+       g_autoptr(struct_gpiod_line_config) line_cfg = NULL;
+       g_autoptr(struct_gpiod_line_request) request = NULL;
+
+       chip = gpiod_test_open_chip_or_fail(g_gpiosim_chip_get_dev_path(sim));
+       settings = gpiod_test_create_line_settings_or_fail();
+       line_cfg = gpiod_test_create_line_config_or_fail();
+
+       gpiod_line_settings_set_direction(settings,
+                                         GPIOD_LINE_DIRECTION_OUTPUT);
+       gpiod_line_settings_set_output_value(settings,
+                                            GPIOD_LINE_VALUE_INACTIVE);
+       gpiod_test_line_config_add_line_settings_or_fail(line_cfg, offsets, 4,
+                                                        settings);
+
+       request = gpiod_test_chip_request_lines_or_fail(chip, NULL, line_cfg);
+
+       gpiod_line_request_set_value(request, 1, GPIOD_LINE_VALUE_ACTIVE);
+
+       g_assert_cmpint(g_gpiosim_chip_get_value(sim, 0), ==,
+                       G_GPIOSIM_VALUE_INACTIVE);
+       g_assert_cmpint(g_gpiosim_chip_get_value(sim, 1), ==,
+                       G_GPIOSIM_VALUE_ACTIVE);
+       g_assert_cmpint(g_gpiosim_chip_get_value(sim, 3), ==,
+                       G_GPIOSIM_VALUE_INACTIVE);
+       g_assert_cmpint(g_gpiosim_chip_get_value(sim, 4), ==,
+                       G_GPIOSIM_VALUE_INACTIVE);
+}
+
+GPIOD_TEST_CASE(request_survives_parent_chip)
+{
+       static const guint offset = 0;
+
+       g_autoptr(GPIOSimChip) sim = g_gpiosim_chip_new("num-lines", 4, NULL);
+       g_autoptr(struct_gpiod_chip) chip = NULL;
+       g_autoptr(struct_gpiod_line_settings) settings = NULL;
+       g_autoptr(struct_gpiod_line_config) line_cfg = NULL;
+       g_autoptr(struct_gpiod_line_request) request = NULL;
+       gint ret;
+
+       chip = gpiod_test_open_chip_or_fail(g_gpiosim_chip_get_dev_path(sim));
+       settings = gpiod_test_create_line_settings_or_fail();
+       line_cfg = gpiod_test_create_line_config_or_fail();
+
+       gpiod_line_settings_set_direction(settings,
+                                         GPIOD_LINE_DIRECTION_OUTPUT);
+       gpiod_line_settings_set_output_value(settings, GPIOD_LINE_VALUE_ACTIVE);
+       gpiod_test_line_config_add_line_settings_or_fail(line_cfg, &offset, 1,
+                                                        settings);
+
+       request = gpiod_test_chip_request_lines_or_fail(chip, NULL, line_cfg);
+
+       g_assert_cmpint(gpiod_line_request_get_value(request, offset), ==,
+                       GPIOD_LINE_VALUE_ACTIVE);
+
+       gpiod_chip_close(chip);
+       chip = NULL;
+
+       ret = gpiod_line_request_set_value(request, offset,
+                                          GPIOD_LINE_VALUE_ACTIVE);
+       g_assert_cmpint(ret, ==, 0);
+       gpiod_test_return_if_failed();
+
+       ret = gpiod_line_request_set_value(request, offset,
+                                          GPIOD_LINE_VALUE_ACTIVE);
+       g_assert_cmpint(ret, ==, 0);
+       gpiod_test_return_if_failed();
+}
+
+GPIOD_TEST_CASE(num_lines_and_offsets)
+{
+       static const guint offsets[] = { 0, 1, 2, 3, 7, 8, 11, 14 };
+
+       g_autoptr(GPIOSimChip) sim = g_gpiosim_chip_new("num-lines", 16, NULL);
+       g_autoptr(struct_gpiod_chip) chip = NULL;
+       g_autoptr(struct_gpiod_line_config) line_cfg = NULL;
+       g_autoptr(struct_gpiod_line_request) request = NULL;
+       guint read_back[8], i;
+
+       chip = gpiod_test_open_chip_or_fail(g_gpiosim_chip_get_dev_path(sim));
+       line_cfg = gpiod_test_create_line_config_or_fail();
+
+       gpiod_test_line_config_add_line_settings_or_fail(line_cfg, offsets, 8,
+                                                        NULL);
+
+       request = gpiod_test_chip_request_lines_or_fail(chip, NULL, line_cfg);
+
+       g_assert_cmpuint(gpiod_line_request_get_num_requested_lines(request),
+                        ==, 8);
+       gpiod_test_return_if_failed();
+       gpiod_line_request_get_requested_offsets(request, read_back, 8);
+       for (i = 0; i < 8; i++)
+               g_assert_cmpuint(read_back[i], ==, offsets[i]);
+}
+
+GPIOD_TEST_CASE(active_low_read_value)
+{
+       g_autoptr(GPIOSimChip) sim = g_gpiosim_chip_new("num-lines", 8, NULL);
+       g_autoptr(struct_gpiod_chip) chip = NULL;
+       g_autoptr(struct_gpiod_line_settings) settings = NULL;
+       g_autoptr(struct_gpiod_line_config) line_cfg = NULL;
+       g_autoptr(struct_gpiod_line_request) request = NULL;
+       guint offset;
+       gint value;
+
+       chip = gpiod_test_open_chip_or_fail(g_gpiosim_chip_get_dev_path(sim));
+       settings = gpiod_test_create_line_settings_or_fail();
+       line_cfg = gpiod_test_create_line_config_or_fail();
+
+       gpiod_line_settings_set_active_low(settings, true);
+       gpiod_line_settings_set_direction(settings, GPIOD_LINE_DIRECTION_INPUT);
+       offset = 2;
+       gpiod_test_line_config_add_line_settings_or_fail(line_cfg, &offset, 1,
+                                                        settings);
+       gpiod_line_settings_set_direction(settings,
+                                         GPIOD_LINE_DIRECTION_OUTPUT);
+       gpiod_line_settings_set_output_value(settings, GPIOD_LINE_VALUE_ACTIVE);
+       offset = 3;
+       gpiod_test_line_config_add_line_settings_or_fail(line_cfg, &offset, 1,
+                                                        settings);
+
+       request = gpiod_test_chip_request_lines_or_fail(chip, NULL, line_cfg);
+
+       g_gpiosim_chip_set_pull(sim, 2, G_GPIOSIM_PULL_DOWN);
+       value = gpiod_line_request_get_value(request, 2);
+       g_assert_cmpint(value, ==, GPIOD_LINE_VALUE_ACTIVE);
+
+       g_assert_cmpint(g_gpiosim_chip_get_value(sim, 3), ==,
+                       G_GPIOSIM_VALUE_INACTIVE);
+}
+
+GPIOD_TEST_CASE(reconfigure_lines)
+{
+       g_autoptr(GPIOSimChip) sim = g_gpiosim_chip_new("num-lines", 4, NULL);
+       g_autoptr(struct_gpiod_chip) chip = NULL;
+       g_autoptr(struct_gpiod_line_settings) settings = NULL;
+       g_autoptr(struct_gpiod_line_config) line_cfg = NULL;
+       g_autoptr(struct_gpiod_line_request) request = NULL;
+       guint offsets[2];
+       gint ret;
+
+       chip = gpiod_test_open_chip_or_fail(g_gpiosim_chip_get_dev_path(sim));
+       settings = gpiod_test_create_line_settings_or_fail();
+       line_cfg = gpiod_test_create_line_config_or_fail();
+
+       gpiod_line_settings_set_direction(settings,
+                                         GPIOD_LINE_DIRECTION_OUTPUT);
+
+       gpiod_line_settings_set_output_value(settings, GPIOD_LINE_VALUE_ACTIVE);
+       offsets[0] = 0;
+       offsets[1] = 2;
+       gpiod_test_line_config_add_line_settings_or_fail(line_cfg, offsets, 2,
+                                                        settings);
+       gpiod_line_settings_set_output_value(settings,
+                                            GPIOD_LINE_VALUE_INACTIVE);
+       offsets[0] = 1;
+       offsets[1] = 3;
+       gpiod_test_line_config_add_line_settings_or_fail(line_cfg, offsets, 2,
+                                                        settings);
+
+       request = gpiod_test_chip_request_lines_or_fail(chip, NULL, line_cfg);
+
+       g_assert_cmpint(g_gpiosim_chip_get_value(sim, 0), ==,
+                       G_GPIOSIM_VALUE_ACTIVE);
+       g_assert_cmpint(g_gpiosim_chip_get_value(sim, 1), ==,
+                       G_GPIOSIM_VALUE_INACTIVE);
+       g_assert_cmpint(g_gpiosim_chip_get_value(sim, 2), ==,
+                       G_GPIOSIM_VALUE_ACTIVE);
+       g_assert_cmpint(g_gpiosim_chip_get_value(sim, 3), ==,
+                       G_GPIOSIM_VALUE_INACTIVE);
+
+       gpiod_line_config_reset(line_cfg);
+
+       gpiod_line_settings_set_output_value(settings,
+                                            GPIOD_LINE_VALUE_INACTIVE);
+       offsets[0] = 0;
+       offsets[1] = 2;
+       gpiod_test_line_config_add_line_settings_or_fail(line_cfg, offsets, 2,
+                                                        settings);
+       gpiod_line_settings_set_output_value(settings, GPIOD_LINE_VALUE_ACTIVE);
+       offsets[0] = 1;
+       offsets[1] = 3;
+       gpiod_test_line_config_add_line_settings_or_fail(line_cfg, offsets, 2,
+                                                        settings);
+
+       ret = gpiod_line_request_reconfigure_lines(request, line_cfg);
+       g_assert_cmpint(ret, ==, 0);
+       gpiod_test_return_if_failed();
+
+       g_assert_cmpint(g_gpiosim_chip_get_value(sim, 0), ==,
+                       G_GPIOSIM_VALUE_INACTIVE);
+       g_assert_cmpint(g_gpiosim_chip_get_value(sim, 1), ==,
+                       G_GPIOSIM_VALUE_ACTIVE);
+       g_assert_cmpint(g_gpiosim_chip_get_value(sim, 2), ==,
+                       G_GPIOSIM_VALUE_INACTIVE);
+       g_assert_cmpint(g_gpiosim_chip_get_value(sim, 3), ==,
+                       G_GPIOSIM_VALUE_ACTIVE);
+}
+
+GPIOD_TEST_CASE(reconfigure_lines_null_config)
+{
+       static const guint offsets[] = { 0, 1, 2, 3 };
+
+       g_autoptr(GPIOSimChip) sim = g_gpiosim_chip_new("num-lines", 8, NULL);
+       g_autoptr(struct_gpiod_chip) chip = NULL;
+       g_autoptr(struct_gpiod_line_config) line_cfg = NULL;
+       g_autoptr(struct_gpiod_line_request) request = NULL;
+       gint ret;
+
+       chip = gpiod_test_open_chip_or_fail(g_gpiosim_chip_get_dev_path(sim));
+       line_cfg = gpiod_test_create_line_config_or_fail();
+
+       gpiod_test_line_config_add_line_settings_or_fail(line_cfg, offsets, 4,
+                                                        NULL);
+
+       request = gpiod_test_chip_request_lines_or_fail(chip, NULL, line_cfg);
+
+       ret = gpiod_line_request_reconfigure_lines(request, NULL);
+       g_assert_cmpint(ret, ==, -1);
+       gpiod_test_expect_errno(EINVAL);
+}
+
+GPIOD_TEST_CASE(reconfigure_lines_different_offsets)
+{
+       static const guint offsets0[] = { 0, 1, 2, 3 };
+       static const guint offsets1[] = { 2, 4, 5 };
+
+       g_autoptr(GPIOSimChip) sim = g_gpiosim_chip_new("num-lines", 8, NULL);
+       g_autoptr(struct_gpiod_chip) chip = NULL;
+       g_autoptr(struct_gpiod_line_config) line_cfg = NULL;
+       g_autoptr(struct_gpiod_line_request) request = NULL;
+       gint ret;
+
+       chip = gpiod_test_open_chip_or_fail(g_gpiosim_chip_get_dev_path(sim));
+       line_cfg = gpiod_test_create_line_config_or_fail();
+
+       gpiod_test_line_config_add_line_settings_or_fail(line_cfg, offsets0, 4,
+                                                        NULL);
+
+       request = gpiod_test_chip_request_lines_or_fail(chip, NULL, line_cfg);
+
+       gpiod_line_config_reset(line_cfg);
+
+       gpiod_test_line_config_add_line_settings_or_fail(line_cfg, offsets1, 3,
+                                                        NULL);
+
+       ret = gpiod_line_request_reconfigure_lines(request, line_cfg);
+       g_assert_cmpint(ret, ==, -1);
+       gpiod_test_expect_errno(EINVAL);
+}
+
+GPIOD_TEST_CASE(request_lines_with_unordered_offsets)
+{
+       static const guint offsets[] = { 5, 1, 7, 2, 0, 6 };
+
+       g_autoptr(GPIOSimChip) sim = g_gpiosim_chip_new("num-lines", 8, NULL);
+       g_autoptr(struct_gpiod_chip) chip = NULL;
+       g_autoptr(struct_gpiod_line_settings) settings = NULL;
+       g_autoptr(struct_gpiod_line_config) line_cfg = NULL;
+       g_autoptr(struct_gpiod_line_request) request = NULL;
+       enum gpiod_line_value values[4];
+       guint set_offsets[4];
+       gint ret;
+
+       chip = gpiod_test_open_chip_or_fail(g_gpiosim_chip_get_dev_path(sim));
+       settings = gpiod_test_create_line_settings_or_fail();
+       line_cfg = gpiod_test_create_line_config_or_fail();
+
+       gpiod_line_settings_set_direction(settings,
+                                         GPIOD_LINE_DIRECTION_OUTPUT);
+       gpiod_line_settings_set_output_value(settings, GPIOD_LINE_VALUE_ACTIVE);
+
+       gpiod_test_line_config_add_line_settings_or_fail(line_cfg, offsets, 6,
+                                                        settings);
+
+       request = gpiod_test_chip_request_lines_or_fail(chip, NULL, line_cfg);
+
+       values[0] = 0;
+       values[1] = 1;
+       values[2] = 0;
+       values[3] = 0;
+       set_offsets[0] = 7;
+       set_offsets[1] = 1;
+       set_offsets[2] = 6;
+       set_offsets[3] = 0;
+       ret = gpiod_line_request_set_values_subset(request, 4, set_offsets, values);
+       g_assert_cmpint(ret, ==, 0);
+       gpiod_test_return_if_failed();
+
+       g_assert_cmpint(g_gpiosim_chip_get_value(sim, 0), ==,
+                       G_GPIOSIM_VALUE_INACTIVE);
+       g_assert_cmpint(g_gpiosim_chip_get_value(sim, 1), ==,
+                       G_GPIOSIM_VALUE_ACTIVE);
+       g_assert_cmpint(g_gpiosim_chip_get_value(sim, 2), ==,
+                       G_GPIOSIM_VALUE_ACTIVE);
+       g_assert_cmpint(g_gpiosim_chip_get_value(sim, 5), ==,
+                       G_GPIOSIM_VALUE_ACTIVE);
+       g_assert_cmpint(g_gpiosim_chip_get_value(sim, 6), ==,
+                       G_GPIOSIM_VALUE_INACTIVE);
+       g_assert_cmpint(g_gpiosim_chip_get_value(sim, 7), ==,
+                       G_GPIOSIM_VALUE_INACTIVE);
+}
+
+GPIOD_TEST_CASE(request_with_bias_set_to_pull_up)
+{
+       static const guint offset = 3;
+
+       g_autoptr(GPIOSimChip) sim = g_gpiosim_chip_new("num-lines", 8, NULL);
+       g_autoptr(struct_gpiod_chip) chip = NULL;
+       g_autoptr(struct_gpiod_line_settings) settings = NULL;
+       g_autoptr(struct_gpiod_line_config) line_cfg = NULL;
+       g_autoptr(struct_gpiod_line_request) request = NULL;
+
+       chip = gpiod_test_open_chip_or_fail(g_gpiosim_chip_get_dev_path(sim));
+       settings = gpiod_test_create_line_settings_or_fail();
+       line_cfg = gpiod_test_create_line_config_or_fail();
+
+       gpiod_line_settings_set_direction(settings, GPIOD_LINE_DIRECTION_INPUT);
+       gpiod_line_settings_set_bias(settings, GPIOD_LINE_BIAS_PULL_UP);
+       gpiod_test_line_config_add_line_settings_or_fail(line_cfg, &offset, 1,
+                                                        settings);
+
+       request = gpiod_test_chip_request_lines_or_fail(chip, NULL, line_cfg);
+
+       g_assert_cmpint(g_gpiosim_chip_get_value(sim, 3), ==,
+                       G_GPIOSIM_VALUE_ACTIVE);
+}
+
+GPIOD_TEST_CASE(get_requested_offsets_less_and_more)
+{
+       static const guint offsets[] = { 0, 1, 2, 3 };
+
+       g_autoptr(GPIOSimChip) sim = g_gpiosim_chip_new("num-lines", 8, NULL);
+       g_autoptr(struct_gpiod_chip) chip = NULL;
+       g_autoptr(struct_gpiod_line_config) line_cfg = NULL;
+       g_autoptr(struct_gpiod_line_request) request = NULL;
+       size_t num_retrieved;
+       guint retrieved[6];
+
+       chip = gpiod_test_open_chip_or_fail(g_gpiosim_chip_get_dev_path(sim));
+       line_cfg = gpiod_test_create_line_config_or_fail();
+
+       gpiod_test_line_config_add_line_settings_or_fail(line_cfg, offsets, 4,
+                                                        NULL);
+
+       request = gpiod_test_chip_request_lines_or_fail(chip, NULL, line_cfg);
+
+       num_retrieved = gpiod_line_request_get_requested_offsets(request,
+                                                                retrieved, 3);
+
+       g_assert_cmpuint(num_retrieved, ==, 3);
+       g_assert_cmpuint(retrieved[0], ==, 0);
+       g_assert_cmpuint(retrieved[1], ==, 1);
+       g_assert_cmpuint(retrieved[2], ==, 2);
+
+       memset(retrieved, 0, sizeof(retrieved));
+
+       num_retrieved = gpiod_line_request_get_requested_offsets(request,
+                                                                retrieved, 6);
+
+       g_assert_cmpuint(num_retrieved, ==, 4);
+       g_assert_cmpuint(retrieved[0], ==, 0);
+       g_assert_cmpuint(retrieved[1], ==, 1);
+       g_assert_cmpuint(retrieved[2], ==, 2);
+       g_assert_cmpuint(retrieved[3], ==, 3);
+}
+
+GPIOD_TEST_CASE(get_chip_name)
+{
+       static const guint offset = 4;
+
+       g_autoptr(GPIOSimChip) sim = g_gpiosim_chip_new("num-lines", 8, NULL);
+       g_autoptr(struct_gpiod_chip) chip = NULL;
+       g_autoptr(struct_gpiod_line_config) line_cfg = NULL;
+       g_autoptr(struct_gpiod_line_request) request = NULL;
+
+       chip = gpiod_test_open_chip_or_fail(g_gpiosim_chip_get_dev_path(sim));
+       line_cfg = gpiod_test_create_line_config_or_fail();
+
+       gpiod_test_line_config_add_line_settings_or_fail(line_cfg, &offset, 1,
+                                                        NULL);
+
+       request = gpiod_test_chip_request_lines_or_fail(chip, NULL, line_cfg);
+
+       g_assert_cmpstr(g_gpiosim_chip_get_name(sim), ==,
+                       gpiod_line_request_get_chip_name(request));
+}
diff --git a/tests/tests-line-settings.c b/tests/tests-line-settings.c
new file mode 100644 (file)
index 0000000..b86fd26
--- /dev/null
@@ -0,0 +1,316 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+// SPDX-FileCopyrightText: 2022 Bartosz Golaszewski <brgl@bgdev.pl>
+
+#include <errno.h>
+#include <glib.h>
+#include <gpiod.h>
+
+#include "gpiod-test.h"
+#include "gpiod-test-helpers.h"
+
+#define GPIOD_TEST_GROUP "line-settings"
+
+GPIOD_TEST_CASE(default_config)
+{
+       g_autoptr(struct_gpiod_line_settings) settings = NULL;
+
+       settings = gpiod_test_create_line_settings_or_fail();
+
+       g_assert_cmpint(gpiod_line_settings_get_direction(settings), ==,
+                       GPIOD_LINE_DIRECTION_AS_IS);
+       g_assert_cmpint(gpiod_line_settings_get_edge_detection(settings), ==,
+                       GPIOD_LINE_EDGE_NONE);
+       g_assert_cmpint(gpiod_line_settings_get_bias(settings), ==,
+                       GPIOD_LINE_BIAS_AS_IS);
+       g_assert_cmpint(gpiod_line_settings_get_drive(settings), ==,
+                       GPIOD_LINE_DRIVE_PUSH_PULL);
+       g_assert_false(gpiod_line_settings_get_active_low(settings));
+       g_assert_cmpuint(gpiod_line_settings_get_debounce_period_us(settings),
+                        ==, 0);
+       g_assert_cmpint(gpiod_line_settings_get_event_clock(settings), ==,
+                       GPIOD_LINE_CLOCK_MONOTONIC);
+       g_assert_cmpint(gpiod_line_settings_get_output_value(settings), ==,
+                       GPIOD_LINE_VALUE_INACTIVE);
+}
+
+GPIOD_TEST_CASE(set_direction)
+{
+       g_autoptr(struct_gpiod_line_settings) settings = NULL;
+       gint ret;
+
+       settings = gpiod_test_create_line_settings_or_fail();
+
+       ret = gpiod_line_settings_set_direction(settings,
+                                               GPIOD_LINE_DIRECTION_INPUT);
+       g_assert_cmpint(ret, ==, 0);
+       g_assert_cmpint(gpiod_line_settings_get_direction(settings), ==,
+                       GPIOD_LINE_DIRECTION_INPUT);
+
+       ret = gpiod_line_settings_set_direction(settings,
+                                               GPIOD_LINE_DIRECTION_AS_IS);
+       g_assert_cmpint(ret, ==, 0);
+       g_assert_cmpint(gpiod_line_settings_get_direction(settings), ==,
+                       GPIOD_LINE_DIRECTION_AS_IS);
+
+       ret = gpiod_line_settings_set_direction(settings,
+                                               GPIOD_LINE_DIRECTION_OUTPUT);
+       g_assert_cmpint(ret, ==, 0);
+       g_assert_cmpint(gpiod_line_settings_get_direction(settings), ==,
+                       GPIOD_LINE_DIRECTION_OUTPUT);
+
+       ret = gpiod_line_settings_set_direction(settings, 999);
+       g_assert_cmpint(ret, <, 0);
+       g_assert_cmpint(errno, ==, EINVAL);
+       g_assert_cmpint(gpiod_line_settings_get_direction(settings), ==,
+                       GPIOD_LINE_DIRECTION_AS_IS);
+}
+
+GPIOD_TEST_CASE(set_edge_detection)
+{
+       g_autoptr(struct_gpiod_line_settings) settings = NULL;
+       gint ret;
+
+       settings = gpiod_test_create_line_settings_or_fail();
+
+       ret = gpiod_line_settings_set_edge_detection(settings,
+                                                    GPIOD_LINE_EDGE_BOTH);
+       g_assert_cmpint(ret, ==, 0);
+       g_assert_cmpint(gpiod_line_settings_get_edge_detection(settings), ==,
+                       GPIOD_LINE_EDGE_BOTH);
+
+       ret = gpiod_line_settings_set_edge_detection(settings,
+                                                    GPIOD_LINE_EDGE_NONE);
+       g_assert_cmpint(ret, ==, 0);
+       g_assert_cmpint(gpiod_line_settings_get_edge_detection(settings), ==,
+                       GPIOD_LINE_EDGE_NONE);
+
+       ret = gpiod_line_settings_set_edge_detection(settings,
+                                                    GPIOD_LINE_EDGE_FALLING);
+       g_assert_cmpint(ret, ==, 0);
+       g_assert_cmpint(gpiod_line_settings_get_edge_detection(settings), ==,
+                       GPIOD_LINE_EDGE_FALLING);
+
+       ret = gpiod_line_settings_set_edge_detection(settings,
+                                                    GPIOD_LINE_EDGE_RISING);
+       g_assert_cmpint(ret, ==, 0);
+       g_assert_cmpint(gpiod_line_settings_get_edge_detection(settings), ==,
+                       GPIOD_LINE_EDGE_RISING);
+
+       ret = gpiod_line_settings_set_edge_detection(settings, 999);
+       g_assert_cmpint(ret, <, 0);
+       g_assert_cmpint(errno, ==, EINVAL);
+       g_assert_cmpint(gpiod_line_settings_get_edge_detection(settings), ==,
+                       GPIOD_LINE_EDGE_NONE);
+}
+
+GPIOD_TEST_CASE(set_bias)
+{
+       g_autoptr(struct_gpiod_line_settings) settings = NULL;
+       gint ret;
+
+       settings = gpiod_test_create_line_settings_or_fail();
+
+       ret = gpiod_line_settings_set_bias(settings, GPIOD_LINE_BIAS_DISABLED);
+       g_assert_cmpint(ret, ==, 0);
+       g_assert_cmpint(gpiod_line_settings_get_bias(settings), ==,
+                       GPIOD_LINE_BIAS_DISABLED);
+
+       ret = gpiod_line_settings_set_bias(settings, GPIOD_LINE_BIAS_AS_IS);
+       g_assert_cmpint(ret, ==, 0);
+       g_assert_cmpint(gpiod_line_settings_get_bias(settings), ==,
+                       GPIOD_LINE_BIAS_AS_IS);
+
+       ret = gpiod_line_settings_set_bias(settings, GPIOD_LINE_BIAS_PULL_DOWN);
+       g_assert_cmpint(ret, ==, 0);
+       g_assert_cmpint(gpiod_line_settings_get_bias(settings), ==,
+                       GPIOD_LINE_BIAS_PULL_DOWN);
+
+       ret = gpiod_line_settings_set_bias(settings, GPIOD_LINE_BIAS_PULL_UP);
+       g_assert_cmpint(ret, ==, 0);
+       g_assert_cmpint(gpiod_line_settings_get_bias(settings), ==,
+                       GPIOD_LINE_BIAS_PULL_UP);
+
+       ret = gpiod_line_settings_set_bias(settings, GPIOD_LINE_BIAS_UNKNOWN);
+       g_assert_cmpint(ret, <, 0);
+       g_assert_cmpint(errno, ==, EINVAL);
+       g_assert_cmpint(gpiod_line_settings_get_bias(settings), ==,
+                       GPIOD_LINE_BIAS_AS_IS);
+
+       ret = gpiod_line_settings_set_bias(settings, 999);
+       g_assert_cmpint(ret, <, 0);
+       g_assert_cmpint(errno, ==, EINVAL);
+       g_assert_cmpint(gpiod_line_settings_get_bias(settings), ==,
+                       GPIOD_LINE_BIAS_AS_IS);
+}
+
+GPIOD_TEST_CASE(set_drive)
+{
+       g_autoptr(struct_gpiod_line_settings) settings = NULL;
+       gint ret;
+
+       settings = gpiod_test_create_line_settings_or_fail();
+
+       ret = gpiod_line_settings_set_drive(settings,
+                                           GPIOD_LINE_DRIVE_OPEN_DRAIN);
+       g_assert_cmpint(ret, ==, 0);
+       g_assert_cmpint(gpiod_line_settings_get_drive(settings), ==,
+                       GPIOD_LINE_DRIVE_OPEN_DRAIN);
+
+       ret = gpiod_line_settings_set_drive(settings,
+                                           GPIOD_LINE_DRIVE_PUSH_PULL);
+       g_assert_cmpint(ret, ==, 0);
+       g_assert_cmpint(gpiod_line_settings_get_drive(settings), ==,
+                       GPIOD_LINE_DRIVE_PUSH_PULL);
+
+       ret = gpiod_line_settings_set_drive(settings,
+                                           GPIOD_LINE_DRIVE_OPEN_SOURCE);
+       g_assert_cmpint(ret, ==, 0);
+       g_assert_cmpint(gpiod_line_settings_get_drive(settings), ==,
+                       GPIOD_LINE_DRIVE_OPEN_SOURCE);
+
+       ret = gpiod_line_settings_set_drive(settings, 999);
+       g_assert_cmpint(ret, <, 0);
+       g_assert_cmpint(errno, ==, EINVAL);
+       g_assert_cmpint(gpiod_line_settings_get_drive(settings), ==,
+                       GPIOD_LINE_DRIVE_PUSH_PULL);
+}
+
+GPIOD_TEST_CASE(set_active_low)
+{
+       g_autoptr(struct_gpiod_line_settings) settings = NULL;
+
+       settings = gpiod_test_create_line_settings_or_fail();
+
+       gpiod_line_settings_set_active_low(settings, true);
+       g_assert_true(gpiod_line_settings_get_active_low(settings));
+
+       gpiod_line_settings_set_active_low(settings, false);
+       g_assert_false(gpiod_line_settings_get_active_low(settings));
+}
+
+GPIOD_TEST_CASE(set_debounce_period)
+{
+       g_autoptr(struct_gpiod_line_settings) settings = NULL;
+
+       settings = gpiod_test_create_line_settings_or_fail();
+
+       gpiod_line_settings_set_debounce_period_us(settings, 4000);
+       g_assert_cmpint(gpiod_line_settings_get_debounce_period_us(settings),
+                       ==, 4000);
+}
+
+GPIOD_TEST_CASE(set_event_clock)
+{
+       g_autoptr(struct_gpiod_line_settings) settings = NULL;
+       gint ret;
+
+       settings = gpiod_test_create_line_settings_or_fail();
+
+       ret = gpiod_line_settings_set_event_clock(settings,
+                                       GPIOD_LINE_CLOCK_MONOTONIC);
+       g_assert_cmpint(ret, ==, 0);
+       g_assert_cmpint(gpiod_line_settings_get_event_clock(settings), ==,
+                       GPIOD_LINE_CLOCK_MONOTONIC);
+
+       ret = gpiod_line_settings_set_event_clock(settings,
+                                       GPIOD_LINE_CLOCK_REALTIME);
+       g_assert_cmpint(ret, ==, 0);
+       g_assert_cmpint(gpiod_line_settings_get_event_clock(settings), ==,
+                       GPIOD_LINE_CLOCK_REALTIME);
+
+       ret = gpiod_line_settings_set_event_clock(settings,
+                                       GPIOD_LINE_CLOCK_HTE);
+       g_assert_cmpint(ret, ==, 0);
+       g_assert_cmpint(gpiod_line_settings_get_event_clock(settings), ==,
+                       GPIOD_LINE_CLOCK_HTE);
+
+       ret = gpiod_line_settings_set_event_clock(settings, 999);
+       g_assert_cmpint(ret, <, 0);
+       g_assert_cmpint(errno, ==, EINVAL);
+       g_assert_cmpint(gpiod_line_settings_get_event_clock(settings), ==,
+                       GPIOD_LINE_CLOCK_MONOTONIC);
+}
+
+GPIOD_TEST_CASE(set_output_value)
+{
+       g_autoptr(struct_gpiod_line_settings) settings = NULL;
+       gint ret;
+
+       settings = gpiod_test_create_line_settings_or_fail();
+
+       ret = gpiod_line_settings_set_output_value(settings,
+                                                  GPIOD_LINE_VALUE_ACTIVE);
+       g_assert_cmpint(ret, ==, 0);
+       g_assert_cmpint(gpiod_line_settings_get_output_value(settings), ==,
+                       GPIOD_LINE_VALUE_ACTIVE);
+
+       ret = gpiod_line_settings_set_output_value(settings,
+                                                  GPIOD_LINE_VALUE_INACTIVE);
+       g_assert_cmpint(ret, ==, 0);
+       g_assert_cmpint(gpiod_line_settings_get_output_value(settings), ==,
+                       GPIOD_LINE_VALUE_INACTIVE);
+
+       ret = gpiod_line_settings_set_output_value(settings, 999);
+       g_assert_cmpint(ret, <, 0);
+       g_assert_cmpint(errno, ==, EINVAL);
+       g_assert_cmpint(gpiod_line_settings_get_output_value(settings), ==,
+                       GPIOD_LINE_VALUE_INACTIVE);
+}
+
+GPIOD_TEST_CASE(copy_line_settings)
+{
+       g_autoptr(struct_gpiod_line_settings) settings = NULL;
+       g_autoptr(struct_gpiod_line_settings) copy = NULL;
+
+       settings = gpiod_test_create_line_settings_or_fail();
+
+       gpiod_line_settings_set_direction(settings, GPIOD_LINE_DIRECTION_INPUT);
+       gpiod_line_settings_set_edge_detection(settings, GPIOD_LINE_EDGE_BOTH);
+       gpiod_line_settings_set_debounce_period_us(settings, 2000);
+       gpiod_line_settings_set_event_clock(settings,
+                                           GPIOD_LINE_CLOCK_REALTIME);
+
+       copy = gpiod_line_settings_copy(settings);
+       g_assert_nonnull(copy);
+       gpiod_test_return_if_failed();
+       g_assert_false(settings == copy);
+       g_assert_cmpint(gpiod_line_settings_get_direction(copy), ==,
+                       GPIOD_LINE_DIRECTION_INPUT);
+       g_assert_cmpint(gpiod_line_settings_get_edge_detection(copy), ==,
+                       GPIOD_LINE_EDGE_BOTH);
+       g_assert_cmpint(gpiod_line_settings_get_debounce_period_us(copy), ==,
+                       2000);
+       g_assert_cmpint(gpiod_line_settings_get_event_clock(copy), ==,
+                       GPIOD_LINE_CLOCK_REALTIME);
+}
+
+GPIOD_TEST_CASE(reset_settings)
+{
+       g_autoptr(struct_gpiod_line_settings) settings = NULL;
+
+       settings = gpiod_test_create_line_settings_or_fail();
+
+       gpiod_line_settings_set_direction(settings, GPIOD_LINE_DIRECTION_INPUT);
+       gpiod_line_settings_set_edge_detection(settings, GPIOD_LINE_EDGE_BOTH);
+       gpiod_line_settings_set_debounce_period_us(settings, 2000);
+       gpiod_line_settings_set_event_clock(settings,
+                                           GPIOD_LINE_CLOCK_REALTIME);
+
+       gpiod_line_settings_reset(settings);
+
+       g_assert_cmpint(gpiod_line_settings_get_direction(settings), ==,
+                       GPIOD_LINE_DIRECTION_AS_IS);
+       g_assert_cmpint(gpiod_line_settings_get_edge_detection(settings), ==,
+                       GPIOD_LINE_EDGE_NONE);
+       g_assert_cmpint(gpiod_line_settings_get_bias(settings), ==,
+                       GPIOD_LINE_BIAS_AS_IS);
+       g_assert_cmpint(gpiod_line_settings_get_drive(settings), ==,
+                       GPIOD_LINE_DRIVE_PUSH_PULL);
+       g_assert_false(gpiod_line_settings_get_active_low(settings));
+       g_assert_cmpuint(gpiod_line_settings_get_debounce_period_us(settings),
+                        ==, 0);
+       g_assert_cmpint(gpiod_line_settings_get_event_clock(settings), ==,
+                       GPIOD_LINE_CLOCK_MONOTONIC);
+       g_assert_cmpint(gpiod_line_settings_get_output_value(settings), ==,
+                       GPIOD_LINE_VALUE_INACTIVE);
+}
diff --git a/tests/tests-misc.c b/tests/tests-misc.c
new file mode 100644 (file)
index 0000000..240dd02
--- /dev/null
@@ -0,0 +1,99 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+// SPDX-FileCopyrightText: 2017-2021 Bartosz Golaszewski <bartekgola@gmail.com>
+
+#include <errno.h>
+#include <glib.h>
+#include <gpiod.h>
+#include <unistd.h>
+
+#include "gpiod-test.h"
+#include "gpiod-test-helpers.h"
+#include "gpiod-test-sim.h"
+
+#define GPIOD_TEST_GROUP "misc"
+
+GPIOD_TEST_CASE(is_gpiochip_bad)
+{
+       g_assert_false(gpiod_is_gpiochip_device("/dev/null"));
+       g_assert_cmpint(errno, ==, 0);
+       g_assert_false(gpiod_is_gpiochip_device("/dev/nonexistent"));
+       g_assert_cmpint(errno, ==, 0);
+}
+
+GPIOD_TEST_CASE(is_gpiochip_good)
+{
+       g_autoptr(GPIOSimChip) sim = g_gpiosim_chip_new(NULL);
+
+       g_assert_true(gpiod_is_gpiochip_device(
+                       g_gpiosim_chip_get_dev_path(sim)));
+}
+
+GPIOD_TEST_CASE(is_gpiochip_link_bad)
+{
+       g_autofree gchar *link = NULL;
+       gint ret;
+
+       link = g_strdup_printf("/tmp/gpiod-test-link.%u", getpid());
+       ret = symlink("/dev/null", link);
+       g_assert_cmpint(ret, ==, 0);
+       gpiod_test_return_if_failed();
+
+       g_assert_false(gpiod_is_gpiochip_device(link));
+       ret = unlink(link);
+       g_assert_cmpint(ret, ==, 0);
+}
+
+GPIOD_TEST_CASE(is_gpiochip_link_good)
+{
+       g_autoptr(GPIOSimChip) sim = g_gpiosim_chip_new(NULL);
+       g_autofree gchar *link = NULL;
+       gint ret;
+
+       link = g_strdup_printf("/tmp/gpiod-test-link.%u", getpid());
+       ret = symlink(g_gpiosim_chip_get_dev_path(sim), link);
+       g_assert_cmpint(ret, ==, 0);
+       gpiod_test_return_if_failed();
+
+       g_assert_true(gpiod_is_gpiochip_device(link));
+       ret = unlink(link);
+       g_assert_cmpint(ret, ==, 0);
+}
+
+GPIOD_TEST_CASE(is_gpiochip_null_path)
+{
+       g_assert_false(gpiod_is_gpiochip_device(NULL));
+       gpiod_test_expect_errno(0);
+}
+
+GPIOD_TEST_CASE(version_string)
+{
+       static const gchar *const pattern = "^\\d+\\.\\d+(\\.\\d+|\\-devel|\\-rc\\d+)?$";
+
+       g_autoptr(GError) err = NULL;
+       g_autoptr(GRegex) regex = NULL;
+       g_autoptr(GMatchInfo) match = NULL;
+       g_autofree gchar *res = NULL;
+       const gchar *ver;
+       gboolean ret;
+
+       ver = gpiod_api_version();
+       g_assert_nonnull(ver);
+       gpiod_test_return_if_failed();
+
+       regex = g_regex_new(pattern, 0, 0, &err);
+       g_assert_nonnull(regex);
+       g_assert_no_error(err);
+       gpiod_test_return_if_failed();
+
+       ret = g_regex_match(regex, ver, 0, &match);
+       g_assert_true(ret);
+       gpiod_test_return_if_failed();
+
+       g_assert_true(g_match_info_matches(match));
+       res = g_match_info_fetch(match, 0);
+       g_assert_nonnull(res);
+       g_assert_cmpstr(res, ==, ver);
+       g_match_info_next(match, &err);
+       g_assert_no_error(err);
+       g_assert_false(g_match_info_matches(match));
+}
diff --git a/tests/tests-request-config.c b/tests/tests-request-config.c
new file mode 100644 (file)
index 0000000..d3c679a
--- /dev/null
@@ -0,0 +1,46 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+// SPDX-FileCopyrightText: 2017-2021 Bartosz Golaszewski <bartekgola@gmail.com>
+
+#include <glib.h>
+#include <gpiod.h>
+
+#include "gpiod-test.h"
+#include "gpiod-test-helpers.h"
+
+#define GPIOD_TEST_GROUP "request-config"
+
+GPIOD_TEST_CASE(default_config)
+{
+       g_autoptr(struct_gpiod_request_config) config = NULL;
+
+       config = gpiod_test_create_request_config_or_fail();
+
+       g_assert_null(gpiod_request_config_get_consumer(config));
+       g_assert_cmpuint(gpiod_request_config_get_event_buffer_size(config), ==,
+                        0);
+}
+
+GPIOD_TEST_CASE(set_consumer)
+{
+       g_autoptr(struct_gpiod_request_config) config = NULL;
+
+       config = gpiod_test_create_request_config_or_fail();
+
+       gpiod_request_config_set_consumer(config, "foobar");
+       g_assert_cmpstr(gpiod_request_config_get_consumer(config), ==,
+                       "foobar");
+
+       gpiod_request_config_set_consumer(config, NULL);
+       g_assert_null(gpiod_request_config_get_consumer(config));
+}
+
+GPIOD_TEST_CASE(set_event_buffer_size)
+{
+       g_autoptr(struct_gpiod_request_config) config = NULL;
+
+       config = gpiod_test_create_request_config_or_fail();
+
+       gpiod_request_config_set_event_buffer_size(config, 128);
+       g_assert_cmpuint(gpiod_request_config_get_event_buffer_size(config), ==,
+                        128);
+}
diff --git a/tools/.gitignore b/tools/.gitignore
new file mode 100644 (file)
index 0000000..dfdbc0d
--- /dev/null
@@ -0,0 +1,9 @@
+# SPDX-License-Identifier: GPL-2.0-or-later
+# SPDX-FileCopyrightText: 2017-2021 Bartosz Golaszewski <bartekgola@gmail.com>
+
+gpiodetect
+gpioinfo
+gpioget
+gpioset
+gpiomon
+gpionotify
diff --git a/tools/Makefile.am b/tools/Makefile.am
new file mode 100644 (file)
index 0000000..bf6170a
--- /dev/null
@@ -0,0 +1,25 @@
+# SPDX-License-Identifier: GPL-2.0-or-later
+# SPDX-FileCopyrightText: 2017-2021 Bartosz Golaszewski <bartekgola@gmail.com>
+
+AM_CFLAGS = -I$(top_srcdir)/include/ -include $(top_builddir)/config.h
+AM_CFLAGS += -Wall -Wextra -g -std=gnu89
+
+noinst_LTLIBRARIES = libtools-common.la
+libtools_common_la_SOURCES = tools-common.c tools-common.h
+
+LDADD = libtools-common.la $(top_builddir)/lib/libgpiod.la
+
+if WITH_GPIOSET_INTERACTIVE
+
+AM_CFLAGS += -DGPIOSET_INTERACTIVE $(LIBEDIT_CFLAGS)
+LDADD += $(LIBEDIT_LIBS)
+
+endif
+
+bin_PROGRAMS = gpiodetect gpioinfo gpioget gpioset gpiomon gpionotify
+
+if WITH_TESTS
+
+dist_noinst_SCRIPTS = gpio-tools-test.bash
+
+endif
diff --git a/tools/gpio-tools-test.bash b/tools/gpio-tools-test.bash
new file mode 100755 (executable)
index 0000000..b55c5eb
--- /dev/null
@@ -0,0 +1,3090 @@
+#!/bin/bash
+# SPDX-License-Identifier: GPL-2.0-or-later
+# SPDX-FileCopyrightText: 2017-2021 Bartosz Golaszewski <bartekgola@gmail.com>
+# SPDX-FileCopyrightText: 2022 Kent Gibson <warthog618@gmail.com>
+# SPDX-FileCopyrightText: 2023 Bartosz Golaszewski <bartosz.golaszewski@linaro.org>
+
+# Simple test harness for the gpio-tools.
+
+# Where output from the dut is stored (must be used together
+# with SHUNIT_TMPDIR).
+DUT_OUTPUT=gpio-tools-test-output
+
+# Save the PID of coprocess - otherwise we won't be able to wait for it
+# once it exits as the COPROC_PID will be cleared.
+DUT_PID=""
+
+SOURCE_DIR="$(dirname ${BASH_SOURCE[0]})"
+
+# mappings from local name to system chip name, path, dev name
+declare -A GPIOSIM_CHIP_NAME
+declare -A GPIOSIM_CHIP_PATH
+declare -A GPIOSIM_DEV_NAME
+GPIOSIM_CONFIGFS="/sys/kernel/config/gpio-sim"
+GPIOSIM_SYSFS="/sys/devices/platform/"
+GPIOSIM_APP_NAME="gpio-tools-test"
+
+MIN_KERNEL_VERSION="5.17.4"
+MIN_SHUNIT_VERSION="2.1.8"
+
+# Run the command in $* and fail the test if the command succeeds.
+assert_fail() {
+       $* || return 0
+       fail " '$*': command did not fail as expected"
+}
+
+# Check if the string in $2 matches against the pattern in $1.
+regex_matches() {
+       [[ $2 =~ $1 ]]
+       assertEquals " '$2' did not match '$1':" "0" "$?"
+}
+
+output_contains_line() {
+       assertContains "$1" "$output"
+}
+
+output_is() {
+       assertEquals " output:" "$1" "$output"
+}
+
+num_lines_is() {
+       [ "$1" -eq "0" ] || [ -z "$output" ] && return 0
+       local NUM_LINES=$(echo "$output" | wc -l)
+       assertEquals " number of lines:" "$1" "$NUM_LINES"
+}
+
+status_is() {
+       assertEquals " status:" "$1" "$status"
+}
+
+# Same as above but match against the regex pattern in $1.
+output_regex_match() {
+       [[ "$output" =~ $1 ]]
+       assertEquals " '$output' did not match '$1'" "0" "$?"
+}
+
+gpiosim_chip() {
+       local VAR=$1
+       local NAME=${GPIOSIM_APP_NAME}-$$-${VAR}
+       local DEVPATH=$GPIOSIM_CONFIGFS/$NAME
+       local BANKPATH=$DEVPATH/bank0
+
+       mkdir -p $BANKPATH
+
+       for ARG in $*
+       do
+               local KEY=$(echo $ARG | cut -d"=" -f1)
+               local VAL=$(echo $ARG | cut -d"=" -f2)
+
+               if [ "$KEY" = "num_lines" ]
+               then
+                       echo $VAL > $BANKPATH/num_lines
+               elif [ "$KEY" = "line_name" ]
+               then
+                       local OFFSET=$(echo $VAL | cut -d":" -f1)
+                       local LINENAME=$(echo $VAL | cut -d":" -f2)
+                       local LINEPATH=$BANKPATH/line$OFFSET
+
+                       mkdir -p $LINEPATH
+                       echo $LINENAME > $LINEPATH/name
+               fi
+       done
+
+       echo 1 > $DEVPATH/live
+
+       local chip_name=$(<$BANKPATH/chip_name)
+       GPIOSIM_CHIP_NAME[$1]=$chip_name
+       GPIOSIM_CHIP_PATH[$1]="/dev/$chip_name"
+       GPIOSIM_DEV_NAME[$1]=$(<$DEVPATH/dev_name)
+}
+
+gpiosim_chip_number() {
+       local NAME=${GPIOSIM_CHIP_NAME[$1]}
+       echo ${NAME#"gpiochip"}
+}
+
+gpiosim_chip_symlink() {
+       GPIOSIM_CHIP_LINK="$2/${GPIOSIM_APP_NAME}-$$-lnk"
+       ln -s ${GPIOSIM_CHIP_PATH[$1]} "$GPIOSIM_CHIP_LINK"
+}
+
+gpiosim_chip_symlink_cleanup() {
+       if [ -n "$GPIOSIM_CHIP_LINK" ]
+       then
+               rm "$GPIOSIM_CHIP_LINK"
+       fi
+       unset GPIOSIM_CHIP_LINK
+}
+
+gpiosim_set_pull() {
+       local OFFSET=$2
+       local PULL=$3
+       local DEVNAME=${GPIOSIM_DEV_NAME[$1]}
+       local CHIPNAME=${GPIOSIM_CHIP_NAME[$1]}
+
+       echo $PULL > $GPIOSIM_SYSFS/$DEVNAME/$CHIPNAME/sim_gpio$OFFSET/pull
+}
+
+gpiosim_check_value() {
+       local OFFSET=$2
+       local EXPECTED=$3
+       local DEVNAME=${GPIOSIM_DEV_NAME[$1]}
+       local CHIPNAME=${GPIOSIM_CHIP_NAME[$1]}
+
+       VAL=$(<$GPIOSIM_SYSFS/$DEVNAME/$CHIPNAME/sim_gpio$OFFSET/value)
+       [ "$VAL" = "$EXPECTED" ]
+}
+
+gpiosim_wait_value() {
+       local OFFSET=$2
+       local EXPECTED=$3
+       local DEVNAME=${GPIOSIM_DEV_NAME[$1]}
+       local CHIPNAME=${GPIOSIM_CHIP_NAME[$1]}
+       local PORT=$GPIOSIM_SYSFS/$DEVNAME/$CHIPNAME/sim_gpio$OFFSET/value
+
+       for i in {1..30}; do
+               [ "$(<$PORT)" = "$EXPECTED" ] && return
+               sleep 0.01
+       done
+       return 1
+}
+
+gpiosim_cleanup() {
+       for CHIP in ${!GPIOSIM_CHIP_NAME[@]}
+       do
+               local NAME=${GPIOSIM_APP_NAME}-$$-$CHIP
+
+               local DEVPATH=$GPIOSIM_CONFIGFS/$NAME
+               local BANKPATH=$DEVPATH/bank0
+
+               echo 0 > $DEVPATH/live
+
+               ls $BANKPATH/line* > /dev/null 2>&1
+               if [ "$?" = "0" ]
+               then
+                       for LINE in $(find $BANKPATH/ | grep -E "line[0-9]+$")
+                       do
+                               test -e $LINE/hog && rmdir $LINE/hog
+                               rmdir $LINE
+                       done
+               fi
+
+               rmdir $BANKPATH
+               rmdir $DEVPATH
+       done
+
+       gpiosim_chip_symlink_cleanup
+
+       GPIOSIM_CHIP_NAME=()
+       GPIOSIM_CHIP_PATH=()
+       GPIOSIM_DEV_NAME=()
+}
+
+run_tool() {
+       # Executables to test are expected to be in the same directory as the
+       # testing script.
+       output=$(timeout 10s $SOURCE_DIR/"$@" 2>&1)
+       status=$?
+}
+
+dut_run() {
+       coproc timeout 10s $SOURCE_DIR/"$@" 2>&1
+       DUT_PID=$COPROC_PID
+       read -t1 -n1 -u ${COPROC[0]} DUT_FIRST_CHAR
+}
+
+dut_run_redirect() {
+       coproc timeout 10s $SOURCE_DIR/"$@" > $SHUNIT_TMPDIR/$DUT_OUTPUT 2>&1
+       DUT_PID=$COPROC_PID
+       # give the process time to spin up
+       # FIXME - find a better solution
+       sleep 0.2
+}
+
+dut_read_redirect() {
+       output=$(<$SHUNIT_TMPDIR/$DUT_OUTPUT)
+        local ORIG_IFS="$IFS"
+        IFS=$'\n' lines=($output)
+        IFS="$ORIG_IFS"
+}
+
+dut_read() {
+       local LINE
+       lines=()
+       while read -t 0.2 -u ${COPROC[0]} LINE;
+       do
+               if [ -n "$DUT_FIRST_CHAR" ]
+               then
+                       LINE=${DUT_FIRST_CHAR}${LINE}
+                       unset DUT_FIRST_CHAR
+               fi
+               lines+=("$LINE")
+       done
+       output="${lines[@]}"
+}
+
+dut_readable() {
+       read -t 0 -u ${COPROC[0]} LINE
+}
+
+dut_flush() {
+       local JUNK
+       lines=()
+       output=
+       unset DUT_FIRST_CHAR
+       while read -t 0 -u ${COPROC[0]} JUNK;
+       do
+               read -t 0.1 -u ${COPROC[0]} JUNK || true
+       done
+}
+
+# check the next line of output matches the regex
+dut_regex_match() {
+       PATTERN=$1
+
+       read -t 0.2 -u ${COPROC[0]} LINE || (echo Timeout && false)
+       if [ -n "$DUT_FIRST_CHAR" ]
+       then
+               LINE=${DUT_FIRST_CHAR}${LINE}
+               unset DUT_FIRST_CHAR
+       fi
+       [[ $LINE =~ $PATTERN ]]
+       assertEquals "'$LINE' did not match '$PATTERN'" "0" "$?"
+}
+
+dut_write() {
+       echo $* >&${COPROC[1]}
+}
+
+dut_kill() {
+       SIGNUM=$1
+
+       kill $SIGNUM $DUT_PID
+}
+
+dut_wait() {
+       wait $DUT_PID
+       export status=$?
+       unset DUT_PID
+}
+
+dut_cleanup() {
+        if [ -n "$DUT_PID" ]
+        then
+               kill -SIGTERM $DUT_PID 2> /dev/null
+               wait $DUT_PID || false
+        fi
+        rm -f $SHUNIT_TMPDIR/$DUT_OUTPUT
+}
+
+tearDown() {
+       dut_cleanup
+       gpiosim_cleanup
+}
+
+request_release_line() {
+       $SOURCE_DIR/gpioget -c $* >/dev/null
+}
+
+#
+# gpiodetect test cases
+#
+
+test_gpiodetect_all_chips() {
+       gpiosim_chip sim0 num_lines=4
+       gpiosim_chip sim1 num_lines=8
+       gpiosim_chip sim2 num_lines=16
+
+       local sim0=${GPIOSIM_CHIP_NAME[sim0]}
+       local sim1=${GPIOSIM_CHIP_NAME[sim1]}
+       local sim2=${GPIOSIM_CHIP_NAME[sim2]}
+       local sim0dev=${GPIOSIM_DEV_NAME[sim0]}
+       local sim1dev=${GPIOSIM_DEV_NAME[sim1]}
+       local sim2dev=${GPIOSIM_DEV_NAME[sim2]}
+
+       run_tool gpiodetect
+
+       output_regex_match "$sim0 \[${sim0dev}[-:]node0\] \(4 lines\)"
+       output_regex_match "$sim1 \[${sim1dev}[-:]node0\] \(8 lines\)"
+       output_regex_match "$sim2 \[${sim2dev}[-:]node0\] \(16 lines\)"
+       status_is 0
+
+       # ignoring symlinks
+       local initial_output=$output
+       gpiosim_chip_symlink sim1 /dev
+
+       run_tool gpiodetect
+
+       output_is "$initial_output"
+       status_is 0
+}
+
+test_gpiodetect_a_chip() {
+       gpiosim_chip sim0 num_lines=4
+       gpiosim_chip sim1 num_lines=8
+       gpiosim_chip sim2 num_lines=16
+
+       local sim0=${GPIOSIM_CHIP_NAME[sim0]}
+       local sim1=${GPIOSIM_CHIP_NAME[sim1]}
+       local sim2=${GPIOSIM_CHIP_NAME[sim2]}
+       local sim0dev=${GPIOSIM_DEV_NAME[sim0]}
+       local sim1dev=${GPIOSIM_DEV_NAME[sim1]}
+       local sim2dev=${GPIOSIM_DEV_NAME[sim2]}
+
+       # by name
+       run_tool gpiodetect $sim0
+
+       output_regex_match "$sim0 \[${sim0dev}[-:]node0\] \(4 lines\)"
+       num_lines_is 1
+       status_is 0
+
+       # by path
+       run_tool gpiodetect ${GPIOSIM_CHIP_PATH[sim1]}
+
+       output_regex_match "$sim1 \[${sim1dev}[-:]node0\] \(8 lines\)"
+       num_lines_is 1
+       status_is 0
+
+       # by number
+       run_tool gpiodetect $(gpiosim_chip_number sim2)
+
+       output_regex_match "$sim2 \[${sim2dev}[-:]node0\] \(16 lines\)"
+       num_lines_is 1
+       status_is 0
+
+       # by symlink
+       gpiosim_chip_symlink sim2 .
+       run_tool gpiodetect $GPIOSIM_CHIP_LINK
+
+       output_regex_match "$sim2 \[${sim2dev}[-:]node0\] \(16 lines\)"
+       num_lines_is 1
+       status_is 0
+}
+
+test_gpiodetect_multiple_chips() {
+       gpiosim_chip sim0 num_lines=4
+       gpiosim_chip sim1 num_lines=8
+       gpiosim_chip sim2 num_lines=16
+
+       local sim0=${GPIOSIM_CHIP_NAME[sim0]}
+       local sim1=${GPIOSIM_CHIP_NAME[sim1]}
+       local sim2=${GPIOSIM_CHIP_NAME[sim2]}
+       local sim0dev=${GPIOSIM_DEV_NAME[sim0]}
+       local sim1dev=${GPIOSIM_DEV_NAME[sim1]}
+       local sim2dev=${GPIOSIM_DEV_NAME[sim2]}
+
+       run_tool gpiodetect $sim0 $sim1 $sim2
+
+       output_regex_match "$sim0 \[${sim0dev}[-:]node0\] \(4 lines\)"
+       output_regex_match "$sim1 \[${sim1dev}[-:]node0\] \(8 lines\)"
+       output_regex_match "$sim2 \[${sim2dev}[-:]node0\] \(16 lines\)"
+       num_lines_is 3
+       status_is 0
+}
+
+test_gpiodetect_with_nonexistent_chip() {
+       run_tool gpiodetect nonexistent-chip
+
+       status_is 1
+       output_regex_match \
+".*cannot find GPIO chip character device 'nonexistent-chip'"
+}
+
+#
+# gpioinfo test cases
+#
+
+test_gpioinfo_all_chips() {
+       gpiosim_chip sim0 num_lines=4
+       gpiosim_chip sim1 num_lines=8
+
+       run_tool gpioinfo
+
+       output_contains_line "${GPIOSIM_CHIP_NAME[sim0]} - 4 lines:"
+       output_contains_line "${GPIOSIM_CHIP_NAME[sim1]} - 8 lines:"
+       output_regex_match "\\s+line\\s+0:\\s+unnamed\\s+input"
+       output_regex_match "\\s+line\\s+7:\\s+unnamed\\s+input"
+       status_is 0
+
+       # ignoring symlinks
+       local initial_output=$output
+       gpiosim_chip_symlink sim1 /dev
+
+       run_tool gpioinfo
+
+       output_is "$initial_output"
+       status_is 0
+}
+
+test_gpioinfo_all_chips_with_some_used_lines() {
+       gpiosim_chip sim0 num_lines=4 line_name=1:foo line_name=2:bar
+       gpiosim_chip sim1 num_lines=8 line_name=3:baz line_name=4:xyz
+
+       dut_run gpioset --banner --active-low foo=1 baz=0
+
+       run_tool gpioinfo
+
+       output_contains_line "${GPIOSIM_CHIP_NAME[sim0]} - 4 lines:"
+       output_contains_line "${GPIOSIM_CHIP_NAME[sim1]} - 8 lines:"
+       output_regex_match "\\s+line\\s+0:\\s+unnamed\\s+input"
+       output_regex_match \
+"\\s+line\\s+1:\\s+\"foo\"\\s+output active-low consumer=\"gpioset\""
+       output_regex_match \
+"\\s+line\\s+3:\\s+\"baz\"\\s+output active-low consumer=\"gpioset\""
+       status_is 0
+}
+
+test_gpioinfo_a_chip() {
+       gpiosim_chip sim0 num_lines=8
+       gpiosim_chip sim1 num_lines=4
+
+       local sim1=${GPIOSIM_CHIP_NAME[sim1]}
+
+       # by name
+       run_tool gpioinfo --chip $sim1
+
+       output_contains_line "$sim1 - 4 lines:"
+       output_regex_match "\\s+line\\s+0:\\s+unnamed\\s+input"
+       output_regex_match "\\s+line\\s+1:\\s+unnamed\\s+input"
+       output_regex_match "\\s+line\\s+2:\\s+unnamed\\s+input"
+       output_regex_match "\\s+line\\s+3:\\s+unnamed\\s+input"
+       num_lines_is 5
+       status_is 0
+
+       # by path
+       run_tool gpioinfo --chip $sim1
+
+       output_contains_line "$sim1 - 4 lines:"
+       output_regex_match "\\s+line\\s+0:\\s+unnamed\\s+input"
+       output_regex_match "\\s+line\\s+1:\\s+unnamed\\s+input"
+       output_regex_match "\\s+line\\s+2:\\s+unnamed\\s+input"
+       output_regex_match "\\s+line\\s+3:\\s+unnamed\\s+input"
+       num_lines_is 5
+       status_is 0
+
+       # by number
+       run_tool gpioinfo --chip $sim1
+
+       output_contains_line "$sim1 - 4 lines:"
+       output_regex_match "\\s+line\\s+0:\\s+unnamed\\s+input"
+       output_regex_match "\\s+line\\s+1:\\s+unnamed\\s+input"
+       output_regex_match "\\s+line\\s+2:\\s+unnamed\\s+input"
+       output_regex_match "\\s+line\\s+3:\\s+unnamed\\s+input"
+       num_lines_is 5
+       status_is 0
+
+       # by symlink
+       gpiosim_chip_symlink sim1 .
+       run_tool gpioinfo --chip $GPIOSIM_CHIP_LINK
+
+       output_contains_line "$sim1 - 4 lines:"
+       output_regex_match "\\s+line\\s+0:\\s+unnamed\\s+input"
+       output_regex_match "\\s+line\\s+1:\\s+unnamed\\s+input"
+       output_regex_match "\\s+line\\s+2:\\s+unnamed\\s+input"
+       output_regex_match "\\s+line\\s+3:\\s+unnamed\\s+input"
+       num_lines_is 5
+       status_is 0
+}
+
+test_gpioinfo_a_line() {
+       gpiosim_chip sim0 num_lines=8 line_name=5:bar
+       gpiosim_chip sim1 num_lines=4 line_name=2:bar
+
+       local sim0=${GPIOSIM_CHIP_NAME[sim0]}
+       local sim1=${GPIOSIM_CHIP_NAME[sim1]}
+
+       # by offset
+       run_tool gpioinfo --chip $sim1 2
+
+       output_regex_match "$sim1 2\\s+\"bar\"\\s+input"
+       num_lines_is 1
+       status_is 0
+
+       # by name
+       run_tool gpioinfo bar
+
+       output_regex_match "$sim0 5\\s+\"bar\"\\s+input"
+       num_lines_is 1
+       status_is 0
+
+       # by chip and name
+       run_tool gpioinfo --chip $sim1 2
+
+       output_regex_match "$sim1 2\\s+\"bar\"\\s+input"
+       num_lines_is 1
+       status_is 0
+
+       # unquoted
+       run_tool gpioinfo --unquoted --chip $sim1 2
+
+       output_regex_match "$sim1 2\\s+bar\\s+input"
+       num_lines_is 1
+       status_is 0
+
+}
+
+test_gpioinfo_first_matching_named_line() {
+       gpiosim_chip sim0 num_lines=4 line_name=1:foo line_name=2:bar \
+                                     line_name=3:foobar
+       gpiosim_chip sim1 num_lines=8 line_name=0:baz line_name=2:foobar \
+                                     line_name=4:xyz line_name=7:foobar
+       gpiosim_chip sim2 num_lines=16
+
+       local sim0=${GPIOSIM_CHIP_NAME[sim0]}
+
+       run_tool gpioinfo foobar
+
+       output_regex_match "$sim0 3\\s+\"foobar\"\\s+input"
+       num_lines_is 1
+       status_is 0
+}
+
+test_gpioinfo_multiple_lines() {
+       gpiosim_chip sim0 num_lines=8 line_name=5:bar
+       gpiosim_chip sim1 num_lines=4 line_name=2:baz
+
+       local sim0=${GPIOSIM_CHIP_NAME[sim0]}
+       local sim1=${GPIOSIM_CHIP_NAME[sim1]}
+
+       # by offset
+       run_tool gpioinfo --chip $sim1 1 2
+
+       output_regex_match "$sim1 1\\s+unnamed\\s+input"
+       output_regex_match "$sim1 2\\s+\"baz\"\\s+input"
+       num_lines_is 2
+       status_is 0
+
+       # by name
+       run_tool gpioinfo bar baz
+
+       output_regex_match "$sim0 5\\s+\"bar\"\\s+input"
+       output_regex_match "$sim1 2\\s+\"baz\"\\s+input"
+       num_lines_is 2
+       status_is 0
+
+       # by name and offset
+       run_tool gpioinfo --chip $sim0 bar 3
+
+       output_regex_match "$sim0 5\\s+\"bar\"\\s+input"
+       output_regex_match "$sim0 3\\s+unnamed\\s+input"
+       num_lines_is 2
+       status_is 0
+}
+
+test_gpioinfo_line_attribute_menagerie() {
+       gpiosim_chip sim0 num_lines=4 line_name=1:foo
+       gpiosim_chip sim1 num_lines=8 line_name=3:baz
+
+       local sim0=${GPIOSIM_CHIP_NAME[sim0]}
+       local sim1=${GPIOSIM_CHIP_NAME[sim1]}
+
+       dut_run gpioset --banner --active-low --bias=pull-up --drive=open-source foo=1 baz=0
+
+       run_tool gpioinfo foo baz
+
+       output_regex_match \
+"$sim0 1\\s+\"foo\"\\s+output active-low drive=open-source bias=pull-up consumer=\"gpioset\""
+       output_regex_match \
+"$sim1 3\\s+\"baz\"\\s+output active-low drive=open-source bias=pull-up consumer=\"gpioset\""
+       num_lines_is 2
+       status_is 0
+
+       dut_kill
+       dut_wait
+
+       dut_run gpioset --banner --bias=pull-down --drive=open-drain foo=1 baz=0
+
+       run_tool gpioinfo foo baz
+
+       output_regex_match \
+"$sim0 1\\s+\"foo\"\\s+output drive=open-drain bias=pull-down consumer=\"gpioset\""
+       output_regex_match \
+"$sim1 3\\s+\"baz\"\\s+output drive=open-drain bias=pull-down consumer=\"gpioset\""
+       num_lines_is 2
+       status_is 0
+
+       dut_kill
+       dut_wait
+
+       dut_run gpiomon --banner --bias=disabled --utc -p 10ms foo baz
+
+       run_tool gpioinfo foo baz
+
+       output_regex_match \
+"$sim0 1\\s+\"foo\"\\s+input bias=disabled edges=both event-clock=realtime debounce-period=10ms consumer=\"gpiomon\""
+       output_regex_match \
+"$sim1 3\\s+\"baz\"\\s+input bias=disabled edges=both event-clock=realtime debounce-period=10ms consumer=\"gpiomon\""
+       num_lines_is 2
+       status_is 0
+
+       dut_kill
+       dut_wait
+
+       dut_run gpiomon --banner --edges=rising --localtime foo baz
+
+       run_tool gpioinfo foo baz
+
+       output_regex_match \
+"$sim0 1\\s+\"foo\"\\s+input edges=rising event-clock=realtime consumer=\"gpiomon\""
+       output_regex_match \
+"$sim1 3\\s+\"baz\"\\s+input edges=rising event-clock=realtime consumer=\"gpiomon\""
+       num_lines_is 2
+       status_is 0
+
+       dut_kill
+       dut_wait
+
+       dut_run gpiomon --banner --edges=falling foo baz
+
+       run_tool gpioinfo foo baz
+
+       output_regex_match \
+"$sim0 1\\s+\"foo\"\\s+input edges=falling consumer=\"gpiomon\""
+       output_regex_match \
+"$sim1 3\\s+\"baz\"\\s+input edges=falling consumer=\"gpiomon\""
+       num_lines_is 2
+       status_is 0
+}
+
+test_gpioinfo_with_same_line_twice() {
+       gpiosim_chip sim0 num_lines=8 line_name=1:foo
+
+       local sim0=${GPIOSIM_CHIP_NAME[sim0]}
+
+       # by offset
+       run_tool gpioinfo --chip $sim0 1 1
+
+       output_regex_match "$sim0 1\\s+\"foo\"\\s+input"
+       output_regex_match ".*lines '1' and '1' are the same line"
+       num_lines_is 2
+       status_is 1
+
+       # by name
+       run_tool gpioinfo foo foo
+
+       output_regex_match "$sim0 1\\s+\"foo\"\\s+input"
+       output_regex_match ".*lines 'foo' and 'foo' are the same line"
+       num_lines_is 2
+       status_is 1
+
+       # by name and offset
+       run_tool gpioinfo --chip $sim0 foo 1
+
+       output_regex_match "$sim0 1\\s+\"foo\"\\s+input"
+       output_regex_match ".*lines 'foo' and '1' are the same line"
+       num_lines_is 2
+       status_is 1
+}
+
+test_gpioinfo_all_lines_matching_name() {
+       gpiosim_chip sim0 num_lines=4 line_name=1:foo line_name=2:bar \
+                                     line_name=3:foobar
+       gpiosim_chip sim1 num_lines=8 line_name=0:baz line_name=2:foobar \
+                                     line_name=4:xyz line_name=7:foobar
+       gpiosim_chip sim2 num_lines=16
+
+       local sim0=${GPIOSIM_CHIP_NAME[sim0]}
+       local sim1=${GPIOSIM_CHIP_NAME[sim1]}
+
+       run_tool gpioinfo --strict foobar
+
+       output_regex_match "$sim0 3\\s+\"foobar\"\\s+input"
+       output_regex_match "$sim1 2\\s+\"foobar\"\\s+input"
+       output_regex_match "$sim1 7\\s+\"foobar\"\\s+input"
+       num_lines_is 3
+       status_is 1
+}
+
+test_gpioinfo_with_lines_strictly_by_name() {
+       # not suggesting this setup makes any sense
+       # - just test that we can deal with it
+       gpiosim_chip sim0 num_lines=8 line_name=1:6 line_name=6:1
+
+       local sim0=${GPIOSIM_CHIP_NAME[sim0]}
+
+       # first by offset (to show offsets match first)
+       run_tool gpioinfo --chip $sim0 1 6
+
+       output_regex_match "$sim0 1\\s+\"6\"\\s+input"
+       output_regex_match "$sim0 6\\s+\"1\"\\s+input"
+       num_lines_is 2
+       status_is 0
+
+       # then strictly by name
+       run_tool gpioinfo --by-name --chip $sim0 1
+
+       output_regex_match "$sim0 6\\s+\"1\"\\s+input"
+       num_lines_is 1
+       status_is 0
+}
+
+test_gpioinfo_with_nonexistent_chip() {
+       run_tool gpioinfo --chip nonexistent-chip
+
+       output_regex_match \
+".*cannot find GPIO chip character device 'nonexistent-chip'"
+       status_is 1
+}
+
+test_gpioinfo_with_nonexistent_line() {
+       gpiosim_chip sim0 num_lines=8
+
+       run_tool gpioinfo nonexistent-line
+
+       output_regex_match ".*cannot find line 'nonexistent-line'"
+       status_is 1
+
+       run_tool gpioinfo --chip ${GPIOSIM_CHIP_NAME[sim0]} nonexistent-line
+
+       output_regex_match ".*cannot find line 'nonexistent-line'"
+       status_is 1
+}
+
+test_gpioinfo_with_offset_out_of_range() {
+       gpiosim_chip sim0 num_lines=4
+
+       local sim0=${GPIOSIM_CHIP_NAME[sim0]}
+
+       run_tool gpioinfo --chip $sim0 0 1 2 3 4 5
+
+       output_regex_match "$sim0 0\\s+unnamed\\s+input"
+       output_regex_match "$sim0 1\\s+unnamed\\s+input"
+       output_regex_match "$sim0 2\\s+unnamed\\s+input"
+       output_regex_match "$sim0 3\\s+unnamed\\s+input"
+       output_regex_match ".*offset 4 is out of range on chip '$sim0'"
+       output_regex_match ".*offset 5 is out of range on chip '$sim0'"
+       num_lines_is 6
+       status_is 1
+}
+
+#
+# gpioget test cases
+#
+
+test_gpioget_by_name() {
+       gpiosim_chip sim0 num_lines=8 line_name=1:foo
+
+       gpiosim_set_pull sim0 1 pull-up
+
+       run_tool gpioget foo
+
+       output_is "\"foo\"=active"
+       status_is 0
+
+       run_tool gpioget --unquoted foo
+
+       output_is "foo=active"
+       status_is 0
+}
+
+test_gpioget_by_offset() {
+       gpiosim_chip sim0 num_lines=8
+
+       gpiosim_set_pull sim0 1 pull-up
+
+       run_tool gpioget --chip ${GPIOSIM_CHIP_NAME[sim0]} 1
+
+       output_is "\"1\"=active"
+       status_is 0
+
+       run_tool gpioget --unquoted --chip ${GPIOSIM_CHIP_NAME[sim0]} 1
+
+       output_is "1=active"
+       status_is 0
+}
+
+test_gpioget_by_symlink() {
+       gpiosim_chip sim0 num_lines=8
+       gpiosim_chip_symlink sim0 .
+
+       gpiosim_set_pull sim0 1 pull-up
+
+       run_tool gpioget --chip $GPIOSIM_CHIP_LINK 1
+
+       output_is "\"1\"=active"
+       status_is 0
+}
+
+test_gpioget_by_chip_and_name() {
+       gpiosim_chip sim0 num_lines=8 line_name=1:foo
+       gpiosim_chip sim1 num_lines=8 line_name=3:foo
+
+       gpiosim_set_pull sim1 3 pull-up
+
+       run_tool gpioget --chip ${GPIOSIM_CHIP_NAME[sim1]} foo
+
+       output_is "\"foo\"=active"
+       status_is 0
+
+       run_tool gpioget --unquoted --chip ${GPIOSIM_CHIP_NAME[sim1]} foo
+
+       output_is "foo=active"
+       status_is 0
+}
+
+test_gpioget_first_matching_named_line() {
+       gpiosim_chip sim0 num_lines=4 line_name=1:foo line_name=2:bar \
+                                     line_name=3:foobar line_name=7:foobar
+       gpiosim_chip sim1 num_lines=8 line_name=0:baz line_name=2:foobar \
+                                     line_name=4:xyz
+       gpiosim_chip sim2 num_lines=16
+
+       gpiosim_set_pull sim0 3 pull-up
+
+       run_tool gpioget foobar
+
+       output_is "\"foobar\"=active"
+       status_is 0
+}
+
+test_gpioget_multiple_lines() {
+       gpiosim_chip sim0 num_lines=8
+
+       gpiosim_set_pull sim0 2 pull-up
+       gpiosim_set_pull sim0 3 pull-up
+       gpiosim_set_pull sim0 5 pull-up
+       gpiosim_set_pull sim0 7 pull-up
+
+       run_tool gpioget --unquoted --chip ${GPIOSIM_CHIP_NAME[sim0]} 0 1 2 3 4 5 6 7
+
+       output_is \
+"0=inactive 1=inactive 2=active 3=active 4=inactive 5=active 6=inactive 7=active"
+       status_is 0
+}
+
+test_gpioget_multiple_lines_by_name_and_offset() {
+       gpiosim_chip sim0 num_lines=8 line_name=1:foo line_name=6:bar
+       gpiosim_chip sim1 num_lines=8 line_name=1:baz line_name=3:bar
+       local sim0=${GPIOSIM_CHIP_NAME[sim0]}
+
+       gpiosim_set_pull sim0 1 pull-up
+       gpiosim_set_pull sim0 4 pull-up
+       gpiosim_set_pull sim0 6 pull-up
+
+       run_tool gpioget --chip $sim0 0 foo 4 bar
+
+       output_is "\"0\"=inactive \"foo\"=active \"4\"=active \"bar\"=active"
+       status_is 0
+}
+
+test_gpioget_multiple_lines_across_multiple_chips() {
+       gpiosim_chip sim0 num_lines=4 line_name=1:foo line_name=2:bar
+       gpiosim_chip sim1 num_lines=8 line_name=0:baz line_name=4:xyz
+
+       gpiosim_set_pull sim0 1 pull-up
+       gpiosim_set_pull sim1 4 pull-up
+
+       run_tool gpioget baz bar foo xyz
+
+       output_is "\"baz\"=inactive \"bar\"=inactive \"foo\"=active \"xyz\"=active"
+       status_is 0
+}
+
+test_gpioget_with_numeric_values() {
+       gpiosim_chip sim0 num_lines=8
+
+       gpiosim_set_pull sim0 2 pull-up
+       gpiosim_set_pull sim0 3 pull-up
+       gpiosim_set_pull sim0 5 pull-up
+       gpiosim_set_pull sim0 7 pull-up
+
+       local sim0=${GPIOSIM_CHIP_NAME[sim0]}
+
+       run_tool gpioget --numeric --chip $sim0 0 1 2 3 4 5 6 7
+
+       output_is "0 0 1 1 0 1 0 1"
+       status_is 0
+}
+
+test_gpioget_with_active_low() {
+       gpiosim_chip sim0 num_lines=8
+
+       gpiosim_set_pull sim0 2 pull-up
+       gpiosim_set_pull sim0 3 pull-up
+       gpiosim_set_pull sim0 5 pull-up
+       gpiosim_set_pull sim0 7 pull-up
+
+       local sim0=${GPIOSIM_CHIP_NAME[sim0]}
+
+       run_tool gpioget --active-low --unquoted --chip $sim0 0 1 2 3 4 5 6 7
+
+       output_is \
+"0=active 1=active 2=inactive 3=inactive 4=active 5=inactive 6=active 7=inactive"
+       status_is 0
+}
+
+test_gpioget_with_consumer() {
+       gpiosim_chip sim0 num_lines=4 line_name=1:foo line_name=2:bar
+       gpiosim_chip sim1 num_lines=8 line_name=3:baz line_name=4:xyz
+
+       dut_run gpionotify --banner -F "%l %E %C" foo baz
+
+       run_tool gpioget --consumer gpio-tools-tests foo baz
+       status_is 0
+
+       dut_read
+       output_regex_match "foo requested gpio-tools-tests"
+       output_regex_match "baz requested gpio-tools-tests"
+}
+
+test_gpioget_with_pull_up() {
+       gpiosim_chip sim0 num_lines=8
+
+       gpiosim_set_pull sim0 2 pull-up
+       gpiosim_set_pull sim0 3 pull-up
+       gpiosim_set_pull sim0 5 pull-up
+       gpiosim_set_pull sim0 7 pull-up
+
+       local sim0=${GPIOSIM_CHIP_NAME[sim0]}
+
+       run_tool gpioget --bias=pull-up --unquoted --chip $sim0 0 1 2 3 4 5 6 7
+
+       output_is \
+"0=active 1=active 2=active 3=active 4=active 5=active 6=active 7=active"
+       status_is 0
+}
+
+test_gpioget_with_pull_down() {
+       gpiosim_chip sim0 num_lines=8
+
+       gpiosim_set_pull sim0 2 pull-up
+       gpiosim_set_pull sim0 3 pull-up
+       gpiosim_set_pull sim0 5 pull-up
+       gpiosim_set_pull sim0 7 pull-up
+
+       local sim0=${GPIOSIM_CHIP_NAME[sim0]}
+
+       run_tool gpioget --bias=pull-down --unquoted --chip $sim0 0 1 2 3 4 5 6 7
+
+       output_is \
+"0=inactive 1=inactive 2=inactive 3=inactive 4=inactive 5=inactive 6=inactive 7=inactive"
+       status_is 0
+}
+
+test_gpioget_with_direction_as_is() {
+       gpiosim_chip sim0 num_lines=8 line_name=1:foo
+
+       local sim0=${GPIOSIM_CHIP_NAME[sim0]}
+
+       # flip to output
+       run_tool gpioset -t0 foo=1
+
+       status_is 0
+
+       run_tool gpioinfo foo
+       output_regex_match "$sim0 1\\s+\"foo\"\\s+output"
+       status_is 0
+
+       run_tool gpioget --as-is foo
+       # note gpio-sim reverts line to its pull when released
+       output_is "\"foo\"=inactive"
+       status_is 0
+
+       run_tool gpioinfo foo
+       output_regex_match "$sim0 1\\s+\"foo\"\\s+output"
+       status_is 0
+
+       # whereas the default behaviour forces to input
+       run_tool gpioget foo
+       # note gpio-sim reverts line to its pull when released
+       # (defaults to pull-down)
+       output_is "\"foo\"=inactive"
+       status_is 0
+
+       run_tool gpioinfo foo
+       output_regex_match "$sim0 1\\s+\"foo\"\\s+input"
+       status_is 0
+}
+
+test_gpioget_with_hold_period() {
+       gpiosim_chip sim0 num_lines=8 line_name=1:foo
+
+       # only test parsing - testing the hold-period itself is tricky
+       run_tool gpioget --hold-period=100ms foo
+       output_is "\"foo\"=inactive"
+       status_is 0
+}
+
+test_gpioget_with_strict_named_line_check() {
+       gpiosim_chip sim0 num_lines=4 line_name=1:foo line_name=2:bar \
+                                     line_name=3:foobar
+       gpiosim_chip sim1 num_lines=8 line_name=0:baz line_name=2:foobar \
+                                     line_name=4:xyz line_name=7:foobar
+       gpiosim_chip sim2 num_lines=16
+
+       run_tool gpioget --strict foobar
+
+       output_regex_match ".*line 'foobar' is not unique"
+       status_is 1
+}
+
+test_gpioget_with_lines_by_offset() {
+       # not suggesting this setup makes any sense
+       # - just test that we can deal with it
+       gpiosim_chip sim0 num_lines=8 line_name=1:6 line_name=6:1
+
+       gpiosim_set_pull sim0 1 pull-up
+       gpiosim_set_pull sim0 6 pull-down
+
+       run_tool gpioget --chip ${GPIOSIM_CHIP_NAME[sim0]} 1 6
+
+       output_is "\"1\"=active \"6\"=inactive"
+       status_is 0
+
+       run_tool gpioget --unquoted --chip ${GPIOSIM_CHIP_NAME[sim0]} 1 6
+
+       output_is "1=active 6=inactive"
+       status_is 0
+}
+
+test_gpioget_with_lines_strictly_by_name() {
+       # not suggesting this setup makes any sense
+       # - just test that we can deal with it
+       gpiosim_chip sim0 num_lines=8 line_name=1:6 line_name=6:1
+
+       gpiosim_set_pull sim0 1 pull-up
+       gpiosim_set_pull sim0 6 pull-down
+
+       run_tool gpioget --by-name --chip ${GPIOSIM_CHIP_NAME[sim0]} 1 6
+
+       output_is "\"1\"=inactive \"6\"=active"
+       status_is 0
+
+       run_tool gpioget --by-name --unquoted --chip ${GPIOSIM_CHIP_NAME[sim0]} 1 6
+
+       output_is "1=inactive 6=active"
+       status_is 0
+}
+
+test_gpioget_with_no_arguments() {
+       run_tool gpioget
+
+       output_regex_match ".*at least one GPIO line must be specified"
+       status_is 1
+}
+
+test_gpioget_with_chip_but_no_line_specified() {
+       gpiosim_chip sim0 num_lines=8
+
+       run_tool gpioget --chip ${GPIOSIM_CHIP_NAME[sim0]}
+
+       output_regex_match ".*at least one GPIO line must be specified"
+       status_is 1
+}
+
+test_gpioget_with_offset_out_of_range() {
+       gpiosim_chip sim0 num_lines=4
+       local sim0=${GPIOSIM_CHIP_NAME[sim0]}
+
+       run_tool gpioget --chip $sim0 0 1 2 3 4 5
+
+       output_regex_match ".*offset 4 is out of range on chip '$sim0'"
+       output_regex_match ".*offset 5 is out of range on chip '$sim0'"
+       status_is 1
+}
+
+test_gpioget_with_nonexistent_line() {
+       run_tool gpioget nonexistent-line
+
+       output_regex_match ".*cannot find line 'nonexistent-line'"
+       status_is 1
+}
+
+test_gpioget_with_same_line_twice() {
+       gpiosim_chip sim0 num_lines=8 line_name=1:foo
+       local sim0=${GPIOSIM_CHIP_NAME[sim0]}
+
+       # by offset
+       run_tool gpioget --chip $sim0 0 0
+
+       output_regex_match ".*lines '0' and '0' are the same line"
+       status_is 1
+
+       # by name
+       run_tool gpioget foo foo
+
+       output_regex_match ".*lines 'foo' and 'foo' are the same line"
+       status_is 1
+
+       # by chip and name
+       run_tool gpioget --chip $sim0 foo foo
+
+       output_regex_match ".*lines 'foo' and 'foo' are the same line"
+       status_is 1
+
+       # by name and offset
+       run_tool gpioget --chip $sim0 foo 1
+
+       output_regex_match ".*lines 'foo' and '1' are the same line"
+       status_is 1
+
+       # by offset and name
+       run_tool gpioget --chip $sim0 1 foo
+
+       output_regex_match ".*lines '1' and 'foo' are the same line"
+       status_is 1
+}
+
+test_gpioget_with_invalid_bias() {
+       gpiosim_chip sim0 num_lines=8
+
+       run_tool gpioget --bias=bad --chip ${GPIOSIM_CHIP_NAME[sim0]} 0 1
+
+       output_regex_match ".*invalid bias.*"
+       status_is 1
+}
+
+test_gpioget_with_invalid_hold_period() {
+       gpiosim_chip sim0 num_lines=8
+
+       run_tool gpioget --hold-period=bad --chip ${GPIOSIM_CHIP_NAME[sim0]} 0
+
+       output_regex_match ".*invalid period.*"
+       status_is 1
+}
+
+#
+# gpioset test cases
+#
+
+test_gpioset_by_name() {
+       gpiosim_chip sim0 num_lines=8 line_name=1:foo
+
+       dut_run gpioset --banner foo=1
+
+       gpiosim_check_value sim0 1 1
+}
+
+test_gpioset_by_offset() {
+       gpiosim_chip sim0 num_lines=8
+
+       dut_run gpioset --banner --chip ${GPIOSIM_CHIP_NAME[sim0]} 1=1
+
+       gpiosim_check_value sim0 1 1
+}
+
+test_gpioset_by_symlink() {
+       gpiosim_chip sim0 num_lines=8
+       gpiosim_chip_symlink sim0 .
+
+       dut_run gpioset --banner --chip $GPIOSIM_CHIP_LINK 1=1
+
+       gpiosim_check_value sim0 1 1
+}
+
+test_gpioset_by_chip_and_name() {
+       gpiosim_chip sim0 num_lines=8 line_name=1:foo
+       gpiosim_chip sim1 num_lines=8 line_name=3:foo
+
+       dut_run gpioset --banner --chip ${GPIOSIM_CHIP_NAME[sim1]} foo=1
+
+       gpiosim_check_value sim1 3 1
+}
+
+test_gpioset_first_matching_named_line() {
+       gpiosim_chip sim0 num_lines=4 line_name=1:foo line_name=2:bar \
+                                     line_name=3:foobar
+       gpiosim_chip sim1 num_lines=8 line_name=0:baz line_name=2:foobar \
+                                     line_name=4:xyz line_name=7:foobar
+       gpiosim_chip sim2 num_lines=16
+
+       dut_run gpioset --banner foobar=1
+
+       gpiosim_check_value sim0 3 1
+}
+
+test_gpioset_multiple_lines() {
+       gpiosim_chip sim0 num_lines=8
+
+       local sim0=${GPIOSIM_CHIP_NAME[sim0]}
+
+       dut_run gpioset --banner --chip $sim0 0=0 1=0 2=1 3=1 4=1 5=1 6=0 7=1
+
+       gpiosim_check_value sim0 0 0
+       gpiosim_check_value sim0 1 0
+       gpiosim_check_value sim0 2 1
+       gpiosim_check_value sim0 3 1
+       gpiosim_check_value sim0 4 1
+       gpiosim_check_value sim0 5 1
+       gpiosim_check_value sim0 6 0
+       gpiosim_check_value sim0 7 1
+}
+
+test_gpioset_multiple_lines_by_name_and_offset() {
+       gpiosim_chip sim0 num_lines=4 line_name=1:foo line_name=2:bar
+
+       dut_run gpioset --banner --chip ${GPIOSIM_CHIP_NAME[sim0]} 0=1 foo=1 bar=1 3=1
+
+       gpiosim_check_value sim0 0 1
+       gpiosim_check_value sim0 1 1
+       gpiosim_check_value sim0 2 1
+       gpiosim_check_value sim0 3 1
+}
+
+
+test_gpioset_multiple_lines_across_multiple_chips() {
+       gpiosim_chip sim0 num_lines=4 line_name=1:foo line_name=2:bar
+       gpiosim_chip sim1 num_lines=8 line_name=0:baz line_name=4:xyz
+
+       dut_run gpioset --banner foo=1 bar=1 baz=1 xyz=1
+
+       gpiosim_check_value sim0 1 1
+       gpiosim_check_value sim0 2 1
+       gpiosim_check_value sim1 0 1
+       gpiosim_check_value sim1 4 1
+}
+
+test_gpioset_with_active_low() {
+       gpiosim_chip sim0 num_lines=8
+       local sim0=${GPIOSIM_CHIP_NAME[sim0]}
+
+       dut_run gpioset --banner --active-low -c $sim0 \
+               0=0 1=0 2=1 3=1 4=1 5=1 6=0 7=1
+
+       gpiosim_check_value sim0 0 1
+       gpiosim_check_value sim0 1 1
+       gpiosim_check_value sim0 2 0
+       gpiosim_check_value sim0 3 0
+       gpiosim_check_value sim0 4 0
+       gpiosim_check_value sim0 5 0
+       gpiosim_check_value sim0 6 1
+       gpiosim_check_value sim0 7 0
+}
+
+test_gpioset_with_consumer() {
+       gpiosim_chip sim0 num_lines=4 line_name=1:foo line_name=2:bar
+       gpiosim_chip sim1 num_lines=8 line_name=3:baz line_name=4:xyz
+
+       dut_run gpioset --banner --consumer gpio-tools-tests foo=1 baz=0
+
+       run_tool gpioinfo
+
+       output_regex_match "\\s+line\\s+0:\\s+unnamed\\s+input"
+       output_regex_match \
+"\\s+line\\s+1:\\s+\"foo\"\\s+output consumer=\"gpio-tools-tests\""
+       output_regex_match \
+"\\s+line\\s+3:\\s+\"baz\"\\s+output consumer=\"gpio-tools-tests\""
+       status_is 0
+}
+
+test_gpioset_with_push_pull() {
+       gpiosim_chip sim0 num_lines=8
+
+       local sim0=${GPIOSIM_CHIP_NAME[sim0]}
+
+       dut_run gpioset --banner --drive=push-pull --chip $sim0 \
+               0=0 1=0 2=1 3=1 4=1 5=1 6=0 7=1
+
+       gpiosim_check_value sim0 0 0
+       gpiosim_check_value sim0 1 0
+       gpiosim_check_value sim0 2 1
+       gpiosim_check_value sim0 3 1
+       gpiosim_check_value sim0 4 1
+       gpiosim_check_value sim0 5 1
+       gpiosim_check_value sim0 6 0
+       gpiosim_check_value sim0 7 1
+}
+
+test_gpioset_with_open_drain() {
+       gpiosim_chip sim0 num_lines=8
+
+       local sim0=${GPIOSIM_CHIP_NAME[sim0]}
+
+       gpiosim_set_pull sim0 2 pull-up
+       gpiosim_set_pull sim0 3 pull-up
+       gpiosim_set_pull sim0 5 pull-up
+       gpiosim_set_pull sim0 7 pull-up
+
+       dut_run gpioset --banner --drive=open-drain --chip $sim0 \
+               0=0 1=0 2=1 3=1 4=1 5=1 6=0 7=1
+
+       gpiosim_check_value sim0 0 0
+       gpiosim_check_value sim0 1 0
+       gpiosim_check_value sim0 2 1
+       gpiosim_check_value sim0 3 1
+       gpiosim_check_value sim0 4 0
+       gpiosim_check_value sim0 5 1
+       gpiosim_check_value sim0 6 0
+       gpiosim_check_value sim0 7 1
+}
+
+test_gpioset_with_open_source() {
+       gpiosim_chip sim0 num_lines=8
+
+       gpiosim_set_pull sim0 2 pull-up
+       gpiosim_set_pull sim0 3 pull-up
+       gpiosim_set_pull sim0 5 pull-up
+       gpiosim_set_pull sim0 7 pull-up
+
+       local sim0=${GPIOSIM_CHIP_NAME[sim0]}
+
+       dut_run gpioset --banner --drive=open-source --chip $sim0 \
+               0=0 1=0 2=1 3=0 4=1 5=1 6=0 7=1
+
+       gpiosim_check_value sim0 0 0
+       gpiosim_check_value sim0 1 0
+       gpiosim_check_value sim0 2 1
+       gpiosim_check_value sim0 3 1
+       gpiosim_check_value sim0 4 1
+       gpiosim_check_value sim0 5 1
+       gpiosim_check_value sim0 6 0
+       gpiosim_check_value sim0 7 1
+}
+
+test_gpioset_with_pull_up() {
+       gpiosim_chip sim0 num_lines=8
+
+       local sim0=${GPIOSIM_CHIP_NAME[sim0]}
+
+       dut_run gpioset --banner --bias=pull-up --drive=open-drain \
+               --chip $sim0 0=0 1=0 2=1 3=0 4=1 5=1 6=0 7=1
+
+       gpiosim_check_value sim0 0 0
+       gpiosim_check_value sim0 1 0
+       gpiosim_check_value sim0 2 1
+       gpiosim_check_value sim0 3 0
+       gpiosim_check_value sim0 4 1
+       gpiosim_check_value sim0 5 1
+       gpiosim_check_value sim0 6 0
+       gpiosim_check_value sim0 7 1
+}
+
+test_gpioset_with_pull_down() {
+       gpiosim_chip sim0 num_lines=8
+
+       local sim0=${GPIOSIM_CHIP_NAME[sim0]}
+
+       dut_run gpioset --banner --bias=pull-down --drive=open-source \
+               --chip $sim0 0=0 1=0 2=1 3=0 4=1 5=1 6=0 7=1
+
+       gpiosim_check_value sim0 0 0
+       gpiosim_check_value sim0 1 0
+       gpiosim_check_value sim0 2 1
+       gpiosim_check_value sim0 3 0
+       gpiosim_check_value sim0 4 1
+       gpiosim_check_value sim0 5 1
+       gpiosim_check_value sim0 6 0
+       gpiosim_check_value sim0 7 1
+}
+
+test_gpioset_with_value_variants() {
+       gpiosim_chip sim0 num_lines=8
+
+       local sim0=${GPIOSIM_CHIP_NAME[sim0]}
+
+       gpiosim_set_pull sim0 0 pull-up
+       gpiosim_set_pull sim0 1 pull-down
+       gpiosim_set_pull sim0 2 pull-down
+       gpiosim_set_pull sim0 3 pull-up
+       gpiosim_set_pull sim0 4 pull-down
+       gpiosim_set_pull sim0 5 pull-up
+       gpiosim_set_pull sim0 6 pull-up
+       gpiosim_set_pull sim0 7 pull-down
+
+       dut_run gpioset --banner --chip $sim0 0=0 1=1 2=active \
+               3=inactive 4=on 5=off 6=false 7=true
+
+       gpiosim_check_value sim0 0 0
+       gpiosim_check_value sim0 1 1
+       gpiosim_check_value sim0 2 1
+       gpiosim_check_value sim0 3 0
+       gpiosim_check_value sim0 4 1
+       gpiosim_check_value sim0 5 0
+       gpiosim_check_value sim0 6 0
+       gpiosim_check_value sim0 7 1
+}
+
+test_gpioset_with_hold_period() {
+       gpiosim_chip sim0 num_lines=8
+
+       local sim0=${GPIOSIM_CHIP_NAME[sim0]}
+
+       gpiosim_set_pull sim0 5 pull-up
+
+       dut_run gpioset --banner --hold-period=1200ms -t0 --chip $sim0 0=1 5=0 7=1
+
+       gpiosim_check_value sim0 0 1
+       gpiosim_check_value sim0 5 0
+       gpiosim_check_value sim0 7 1
+
+       dut_wait
+
+       status_is 0
+}
+
+test_gpioset_interactive_exit() {
+       gpiosim_chip sim0 num_lines=8
+
+       local sim0=${GPIOSIM_CHIP_NAME[sim0]}
+
+       dut_run gpioset --interactive --chip $sim0 1=0 2=1 5=1 6=0 7=1
+
+       gpiosim_check_value sim0 1 0
+       gpiosim_check_value sim0 2 1
+       gpiosim_check_value sim0 5 1
+       gpiosim_check_value sim0 6 0
+       gpiosim_check_value sim0 7 1
+
+       dut_write "exit"
+       dut_wait
+
+       status_is 0
+}
+
+test_gpioset_interactive_help() {
+       gpiosim_chip sim0 num_lines=8 line_name=1:foo line_name=4:bar \
+                                     line_name=7:baz
+
+       dut_run gpioset --interactive foo=1 bar=0 baz=0
+
+       gpiosim_check_value sim0 1 1
+       gpiosim_check_value sim0 4 0
+       gpiosim_check_value sim0 7 0
+
+       dut_write "help"
+
+       dut_read
+       output_regex_match "COMMANDS:.*"
+       output_regex_match ".*get \[line\]\.\.\..*"
+       output_regex_match ".*set <line=value>\.\.\..*"
+       output_regex_match ".*toggle \[line\]\.\.\..*"
+       output_regex_match ".*sleep <period>.*"
+}
+
+test_gpioset_interactive_get() {
+       gpiosim_chip sim0 num_lines=8 line_name=1:foo line_name=4:bar \
+                                     line_name=7:baz
+
+       dut_run gpioset --interactive foo=1 bar=0 baz=0
+
+       gpiosim_check_value sim0 1 1
+       gpiosim_check_value sim0 4 0
+       gpiosim_check_value sim0 7 0
+
+       dut_write "get"
+
+       dut_read
+       output_regex_match "\"foo\"=active \"bar\"=inactive \"baz\"=inactive"
+
+       dut_write "get bar"
+
+       dut_read
+       output_regex_match "\"bar\"=inactive"
+}
+
+test_gpioset_interactive_get_unquoted() {
+       gpiosim_chip sim0 num_lines=8 line_name=1:foo line_name=4:bar \
+                                     line_name=7:baz
+
+       dut_run gpioset --interactive --unquoted foo=1 bar=0 baz=0
+
+       gpiosim_check_value sim0 1 1
+       gpiosim_check_value sim0 4 0
+       gpiosim_check_value sim0 7 0
+
+       dut_write "get"
+
+       dut_read
+       output_regex_match "foo=active bar=inactive baz=inactive"
+
+       dut_write "get bar"
+
+       dut_read
+       output_regex_match "bar=inactive"
+}
+
+test_gpioset_interactive_set() {
+       gpiosim_chip sim0 num_lines=8 line_name=1:foo line_name=4:bar \
+                                     line_name=7:baz
+
+       dut_run gpioset --interactive foo=1 bar=0 baz=0
+
+       gpiosim_check_value sim0 1 1
+       gpiosim_check_value sim0 4 0
+       gpiosim_check_value sim0 7 0
+
+       dut_write "set bar=active"
+
+       gpiosim_check_value sim0 1 1
+       gpiosim_check_value sim0 4 1
+       gpiosim_check_value sim0 7 0
+       dut_write "get"
+       dut_read
+       output_regex_match "\"foo\"=active \"bar\"=active \"baz\"=inactive"
+}
+
+test_gpioset_interactive_toggle() {
+       gpiosim_chip sim0 num_lines=8 line_name=1:foo line_name=4:bar \
+                                     line_name=7:baz
+
+       gpiosim_set_pull sim0 4 pull-up
+       gpiosim_set_pull sim0 7 pull-up
+
+       dut_run gpioset -i foo=1 bar=0 baz=0
+
+       gpiosim_check_value sim0 1 1
+       gpiosim_check_value sim0 4 0
+       gpiosim_check_value sim0 7 0
+
+       dut_write "toggle"
+
+       gpiosim_check_value sim0 1 0
+       gpiosim_check_value sim0 4 1
+       gpiosim_check_value sim0 7 1
+       dut_write "get"
+       dut_read
+       output_regex_match "\"foo\"=inactive\\s+\"bar\"=active\\s+\"baz\"=active\\s*"
+
+       dut_write "toggle baz"
+
+       gpiosim_check_value sim0 1 0
+       gpiosim_check_value sim0 4 1
+       gpiosim_check_value sim0 7 0
+       dut_write "get"
+       dut_read
+       output_regex_match "\"foo\"=inactive\\s+\"bar\"=active\\s+\"baz\"=inactive\\s*"
+}
+
+test_gpioset_interactive_sleep() {
+       gpiosim_chip sim0 num_lines=8 line_name=1:foo line_name=4:bar \
+                                     line_name=7:baz
+
+       dut_run gpioset --interactive foo=1 bar=0 baz=0
+
+       dut_write "sleep 500ms"
+       dut_flush
+
+       assert_fail dut_readable
+
+       sleep 1
+
+       # prompt, but not a full line...
+       dut_readable
+}
+
+test_gpioset_toggle_continuous() {
+       gpiosim_chip sim0 num_lines=8 line_name=1:foo line_name=4:bar \
+                                     line_name=7:baz
+
+       gpiosim_set_pull sim0 4 pull-up
+       gpiosim_set_pull sim0 7 pull-up
+
+       dut_run gpioset --banner --toggle 100ms foo=1 bar=0 baz=0
+
+       gpiosim_check_value sim0 1 1
+       gpiosim_check_value sim0 4 0
+       gpiosim_check_value sim0 7 0
+
+       gpiosim_wait_value sim0 1 0
+       gpiosim_check_value sim0 4 1
+       gpiosim_check_value sim0 7 1
+
+
+       gpiosim_wait_value sim0 1 1
+       gpiosim_check_value sim0 4 0
+       gpiosim_check_value sim0 7 0
+
+       gpiosim_wait_value sim0 1 0
+       gpiosim_check_value sim0 4 1
+       gpiosim_check_value sim0 7 1
+}
+
+test_gpioset_toggle_terminated() {
+       gpiosim_chip sim0 num_lines=8 line_name=1:foo line_name=4:bar \
+                                     line_name=7:baz
+
+       gpiosim_set_pull sim0 4 pull-up
+
+       # hold-period to allow test to sample before gpioset exits
+       dut_run gpioset --banner --toggle 1s,0 -p 600ms foo=1 bar=0 baz=1
+
+       gpiosim_check_value sim0 1 1
+       gpiosim_check_value sim0 4 0
+       gpiosim_check_value sim0 7 1
+
+       sleep 1
+
+       gpiosim_check_value sim0 1 0
+       gpiosim_check_value sim0 4 1
+       gpiosim_check_value sim0 7 0
+
+       dut_wait
+
+       status_is 0
+
+       # using --toggle 0 to exit
+       # hold-period to allow test to sample before gpioset exits
+       dut_run gpioset --banner -t0 -p 600ms foo=1 bar=0 baz=1
+
+       gpiosim_check_value sim0 1 1
+       gpiosim_check_value sim0 4 0
+       gpiosim_check_value sim0 7 1
+
+       dut_wait
+
+       status_is 0
+}
+
+test_gpioset_with_invalid_toggle_period() {
+       gpiosim_chip sim0 num_lines=8 line_name=1:foo line_name=4:bar \
+                                     line_name=7:baz
+
+       run_tool gpioset --toggle 1ns foo=1 bar=0 baz=0
+
+       output_regex_match ".*invalid period.*"
+       status_is 1
+}
+
+test_gpioset_with_strict_named_line_check() {
+       gpiosim_chip sim0 num_lines=4 line_name=1:foo line_name=2:bar \
+                                     line_name=3:foobar
+       gpiosim_chip sim1 num_lines=8 line_name=0:baz line_name=2:foobar \
+                                     line_name=4:xyz line_name=7:foobar
+       gpiosim_chip sim2 num_lines=16
+
+       run_tool gpioset --strict foobar=active
+
+       output_regex_match ".*line 'foobar' is not unique"
+       status_is 1
+}
+
+test_gpioset_with_lines_by_offset() {
+       # not suggesting this setup makes any sense
+       # - just test that we can deal with it
+       gpiosim_chip sim0 num_lines=8 line_name=1:6 line_name=6:1
+
+       gpiosim_set_pull sim0 1 pull-down
+       gpiosim_set_pull sim0 6 pull-up
+
+       dut_run gpioset --banner --chip ${GPIOSIM_CHIP_NAME[sim0]} 6=1 1=0
+
+       gpiosim_check_value sim0 1 0
+       gpiosim_check_value sim0 6 1
+}
+
+test_gpioset_with_lines_strictly_by_name() {
+       # not suggesting this setup makes any sense
+       # - just test that we can deal with it
+       gpiosim_chip sim0 num_lines=8 line_name=1:6 line_name=6:1
+
+       gpiosim_set_pull sim0 1 pull-down
+       gpiosim_set_pull sim0 6 pull-up
+
+       dut_run gpioset --banner --by-name --chip ${GPIOSIM_CHIP_NAME[sim0]} 6=1 1=0
+
+       gpiosim_check_value sim0 1 1
+       gpiosim_check_value sim0 6 0
+}
+
+test_gpioset_interactive_after_SIGINT() {
+       gpiosim_chip sim0 num_lines=8 line_name=1:foo
+
+       dut_run gpioset -i foo=1
+
+       dut_kill -SIGINT
+       dut_wait
+
+       status_is 130
+}
+
+test_gpioset_interactive_after_SIGTERM() {
+       gpiosim_chip sim0 num_lines=8 line_name=1:foo
+
+       dut_run gpioset -i foo=1
+
+       dut_kill -SIGTERM
+       dut_wait
+
+       status_is 143
+}
+
+test_gpioset_with_no_arguments() {
+       run_tool gpioset
+
+       status_is 1
+       output_regex_match ".*at least one GPIO line value must be specified"
+}
+
+test_gpioset_with_chip_but_no_line_specified() {
+       gpiosim_chip sim0 num_lines=8
+
+       run_tool gpioset --chip ${GPIOSIM_CHIP_NAME[sim0]}
+
+       output_regex_match ".*at least one GPIO line value must be specified"
+       status_is 1
+}
+
+test_gpioset_with_offset_out_of_range() {
+       gpiosim_chip sim0 num_lines=4
+
+       local sim0=${GPIOSIM_CHIP_NAME[sim0]}
+
+       run_tool gpioset --chip $sim0 0=1 1=1 2=1 3=1 4=1 5=1
+
+       output_regex_match ".*offset 4 is out of range on chip '$sim0'"
+       output_regex_match ".*offset 5 is out of range on chip '$sim0'"
+       status_is 1
+}
+
+test_gpioset_with_invalid_hold_period() {
+       gpiosim_chip sim0 num_lines=8
+
+       local sim0=${GPIOSIM_CHIP_NAME[sim0]}
+
+       run_tool gpioset --hold-period=bad --chip $sim0 0=1
+
+       output_regex_match ".*invalid period.*"
+       status_is 1
+}
+
+test_gpioset_with_invalid_value() {
+       gpiosim_chip sim0 num_lines=8
+
+       local sim0=${GPIOSIM_CHIP_NAME[sim0]}
+
+       # by name
+       run_tool gpioset --chip $sim0 0=c
+
+       output_regex_match ".*invalid line value.*"
+       status_is 1
+
+       # by value
+       run_tool gpioset --chip $sim0 0=3
+
+       output_regex_match ".*invalid line value.*"
+       status_is 1
+}
+
+test_gpioset_with_invalid_offset() {
+       gpiosim_chip sim0 num_lines=8
+
+       run_tool gpioset --chip ${GPIOSIM_CHIP_NAME[sim0]} 4000000000=0
+
+       output_regex_match ".*cannot find line '4000000000'"
+       status_is 1
+}
+
+test_gpioset_with_invalid_bias() {
+       gpiosim_chip sim0 num_lines=8
+
+       run_tool gpioset --bias=bad --chip ${GPIOSIM_CHIP_NAME[sim0]} 0=1 1=1
+
+       output_regex_match ".*invalid bias.*"
+       status_is 1
+}
+
+test_gpioset_with_invalid_drive() {
+       gpiosim_chip sim0 num_lines=8
+
+       run_tool gpioset --drive=bad --chip ${GPIOSIM_CHIP_NAME[sim0]} 0=1 1=1
+
+       output_regex_match ".*invalid drive.*"
+       status_is 1
+}
+
+test_gpioset_with_interactive_and_toggle() {
+       gpiosim_chip sim0 num_lines=8
+
+       local sim0=${GPIOSIM_CHIP_NAME[sim0]}
+
+       run_tool gpioset --interactive --toggle 1s --chip $sim0 0=1
+
+       output_regex_match ".*can't combine interactive with toggle"
+       status_is 1
+}
+
+test_gpioset_with_nonexistent_line() {
+       run_tool gpioset nonexistent-line=0
+
+       output_regex_match ".*cannot find line 'nonexistent-line'"
+       status_is 1
+}
+
+test_gpioset_with_same_line_twice() {
+       gpiosim_chip sim0 num_lines=8 line_name=1:foo
+
+       local sim0=${GPIOSIM_CHIP_NAME[sim0]}
+
+       # by offset
+       run_tool gpioset --chip $sim0 0=1 0=1
+
+       output_regex_match ".*lines '0' and '0' are the same line"
+       status_is 1
+
+       # by name
+       run_tool gpioset --chip $sim0 foo=1 foo=1
+
+       output_regex_match ".*lines 'foo' and 'foo' are the same line"
+       status_is 1
+
+       # by name and offset
+       run_tool gpioset --chip $sim0 foo=1 1=1
+
+       output_regex_match ".*lines 'foo' and '1' are the same line"
+       status_is 1
+
+       # by offset and name
+       run_tool gpioset --chip $sim0 1=1 foo=1
+
+       output_regex_match ".*lines '1' and 'foo' are the same line"
+       status_is 1
+}
+
+#
+# gpiomon test cases
+#
+
+test_gpiomon_by_name() {
+       gpiosim_chip sim0 num_lines=8 line_name=4:foo
+
+       gpiosim_set_pull sim0 4 pull-up
+
+       dut_run gpiomon --banner --edges=rising foo
+       dut_flush
+
+       gpiosim_set_pull sim0 4 pull-down
+       gpiosim_set_pull sim0 4 pull-up
+       gpiosim_set_pull sim0 4 pull-down
+       dut_regex_match "[0-9]+\.[0-9]+\\s+rising\\s+\"foo\""
+       assert_fail dut_readable
+}
+
+test_gpiomon_by_offset() {
+       gpiosim_chip sim0 num_lines=8
+
+       local sim0=${GPIOSIM_CHIP_NAME[sim0]}
+
+       gpiosim_set_pull sim0 4 pull-up
+
+       dut_run gpiomon --banner --edges=rising --chip $sim0 4
+       dut_regex_match "Monitoring line .*"
+
+       gpiosim_set_pull sim0 4 pull-down
+       gpiosim_set_pull sim0 4 pull-up
+       gpiosim_set_pull sim0 4 pull-down
+       dut_regex_match "[0-9]+\.[0-9]+\\s+rising\\s+$sim0 4"
+       assert_fail dut_readable
+}
+
+test_gpiomon_by_symlink() {
+       gpiosim_chip sim0 num_lines=8
+       gpiosim_chip_symlink sim0 .
+
+       local sim0=${GPIOSIM_CHIP_NAME[sim0]}
+
+       gpiosim_set_pull sim0 4 pull-up
+
+       dut_run gpiomon --banner --edges=rising --chip $GPIOSIM_CHIP_LINK 4
+       dut_regex_match "Monitoring line .*"
+
+       gpiosim_set_pull sim0 4 pull-down
+       gpiosim_set_pull sim0 4 pull-up
+       gpiosim_set_pull sim0 4 pull-down
+       dut_regex_match "[0-9]+\.[0-9]+\\s+rising\\s+$sim0\\s+4"
+       assert_fail dut_readable
+}
+
+
+test_gpiomon_by_chip_and_name() {
+       gpiosim_chip sim0 num_lines=8 line_name=0:foo
+       gpiosim_chip sim1 num_lines=8 line_name=2:foo
+
+       local sim1=${GPIOSIM_CHIP_NAME[sim1]}
+
+       gpiosim_set_pull sim1 0 pull-up
+
+       dut_run gpiomon --banner --edges=rising --chip $sim1 foo
+       dut_regex_match "Monitoring line .*"
+
+       gpiosim_set_pull sim1 2 pull-down
+       gpiosim_set_pull sim1 2 pull-up
+       gpiosim_set_pull sim1 2 pull-down
+       dut_regex_match "[0-9]+\.[0-9]+\\s+rising\\s+$sim1 2 \"foo\""
+       assert_fail dut_readable
+}
+
+test_gpiomon_first_matching_named_line() {
+       gpiosim_chip sim0 num_lines=4 line_name=1:foo line_name=2:bar \
+                                     line_name=3:foobar
+       gpiosim_chip sim1 num_lines=8 line_name=0:baz line_name=2:foobar \
+                                     line_name=4:xyz line_name=7:foobar
+       gpiosim_chip sim2 num_lines=16
+
+       dut_run gpiomon --banner foobar
+       dut_regex_match "Monitoring line .*"
+
+       gpiosim_set_pull sim0 3 pull-up
+       dut_regex_match "[0-9]+\.[0-9]+\\s+rising\\s+\"foobar\""
+       assert_fail dut_readable
+}
+
+test_gpiomon_rising_edge() {
+       gpiosim_chip sim0 num_lines=8
+
+       local sim0=${GPIOSIM_CHIP_NAME[sim0]}
+
+       gpiosim_set_pull sim0 4 pull-up
+
+       dut_run gpiomon --banner --edges=rising --chip $sim0 4
+       dut_flush
+
+       gpiosim_set_pull sim0 4 pull-down
+       gpiosim_set_pull sim0 4 pull-up
+       gpiosim_set_pull sim0 4 pull-down
+       dut_regex_match "[0-9]+\.[0-9]+\\s+rising\\s+$sim0 4"
+       assert_fail dut_readable
+}
+
+test_gpiomon_falling_edge() {
+       gpiosim_chip sim0 num_lines=8
+       local sim0=${GPIOSIM_CHIP_NAME[sim0]}
+
+       gpiosim_set_pull sim0 4 pull-down
+
+       dut_run gpiomon --banner --edges=falling --chip $sim0 4
+       dut_flush
+
+       gpiosim_set_pull sim0 4 pull-up
+       gpiosim_set_pull sim0 4 pull-down
+       gpiosim_set_pull sim0 4 pull-up
+       dut_regex_match "[0-9]+\.[0-9]+\\s+falling\\s+$sim0 4"
+       assert_fail dut_readable
+}
+
+test_gpiomon_both_edges() {
+       gpiosim_chip sim0 num_lines=8
+       local sim0=${GPIOSIM_CHIP_NAME[sim0]}
+
+       dut_run gpiomon --banner --edges=both --chip $sim0 4
+       dut_regex_match "Monitoring line .*"
+
+       gpiosim_set_pull sim0 4 pull-up
+       dut_regex_match "[0-9]+\.[0-9]+\\s+rising\\s+$sim0 4"
+
+       gpiosim_set_pull sim0 4 pull-down
+       dut_regex_match "[0-9]+\.[0-9]+\\s+falling\\s+$sim0 4"
+}
+
+test_gpiomon_with_pull_up() {
+       gpiosim_chip sim0 num_lines=8
+       local sim0=${GPIOSIM_CHIP_NAME[sim0]}
+
+       gpiosim_set_pull sim0 4 pull-down
+
+       dut_run gpiomon --banner --bias=pull-up --chip $sim0 4
+       dut_flush
+
+       gpiosim_set_pull sim0 4 pull-down
+       dut_regex_match "[0-9]+\.[0-9]+\\s+falling\\s+$sim0 4"
+
+       assert_fail dut_readable
+}
+
+test_gpiomon_with_pull_down() {
+       gpiosim_chip sim0 num_lines=8
+       local sim0=${GPIOSIM_CHIP_NAME[sim0]}
+
+       gpiosim_set_pull sim0 4 pull-up
+
+       dut_run gpiomon --banner --bias=pull-down --chip $sim0 4
+       dut_flush
+
+       gpiosim_set_pull sim0 4 pull-up
+
+       dut_regex_match "[0-9]+\.[0-9]+\\s+rising\\s+$sim0 4"
+
+       assert_fail dut_readable
+}
+
+test_gpiomon_with_active_low() {
+       gpiosim_chip sim0 num_lines=8
+       local sim0=${GPIOSIM_CHIP_NAME[sim0]}
+
+       gpiosim_set_pull sim0 4 pull-up
+
+       dut_run gpiomon --banner --active-low --chip $sim0 4
+       dut_flush
+
+       gpiosim_set_pull sim0 4 pull-down
+       dut_regex_match "[0-9]+\.[0-9]+\\s+rising\\s+$sim0 4"
+
+       gpiosim_set_pull sim0 4 pull-up
+       dut_regex_match "[0-9]+\.[0-9]+\\s+falling\\s+$sim0 4"
+
+       assert_fail dut_readable
+}
+
+test_gpiomon_with_consumer() {
+       gpiosim_chip sim0 num_lines=4 line_name=1:foo line_name=2:bar
+       gpiosim_chip sim1 num_lines=8 line_name=3:baz line_name=4:xyz
+
+       dut_run gpiomon --banner --consumer gpio-tools-tests foo baz
+
+       run_tool gpioinfo
+
+       output_regex_match "\\s+line\\s+0:\\s+unnamed\\s+input"
+       output_regex_match \
+"\\s+line\\s+1:\\s+\"foo\"\\s+input edges=both consumer=\"gpio-tools-tests\""
+       output_regex_match \
+"\\s+line\\s+3:\\s+\"baz\"\\s+input edges=both consumer=\"gpio-tools-tests\""
+       status_is 0
+}
+
+test_gpiomon_with_quiet_mode() {
+       gpiosim_chip sim0 num_lines=8
+
+       local sim0=${GPIOSIM_CHIP_NAME[sim0]}
+
+       dut_run gpiomon --banner --edges=rising --quiet --chip $sim0 4
+       dut_flush
+
+       gpiosim_set_pull sim0 4 pull-up
+       assert_fail dut_readable
+}
+
+test_gpiomon_with_unquoted() {
+       gpiosim_chip sim0 num_lines=8 line_name=4:foo
+
+       gpiosim_set_pull sim0 4 pull-up
+
+       dut_run gpiomon --banner --unquoted --edges=rising foo
+       dut_flush
+
+       gpiosim_set_pull sim0 4 pull-down
+       gpiosim_set_pull sim0 4 pull-up
+       gpiosim_set_pull sim0 4 pull-down
+       dut_regex_match "[0-9]+\.[0-9]+\\s+rising\\s+foo"
+       assert_fail dut_readable
+}
+
+test_gpiomon_with_num_events() {
+       gpiosim_chip sim0 num_lines=8
+
+       local sim0=${GPIOSIM_CHIP_NAME[sim0]}
+
+       # redirect, as gpiomon exits after 4 events
+       dut_run_redirect gpiomon --num-events=4 --chip $sim0 4
+
+       gpiosim_set_pull sim0 4 pull-up
+       sleep 0.01
+       gpiosim_set_pull sim0 4 pull-down
+       sleep 0.01
+       gpiosim_set_pull sim0 4 pull-up
+       sleep 0.01
+       gpiosim_set_pull sim0 4 pull-down
+       sleep 0.01
+
+       dut_wait
+       status_is 0
+       dut_read_redirect
+
+       regex_matches "[0-9]+\.[0-9]+\\s+rising\\s+$sim0 4" "${lines[0]}"
+       regex_matches "[0-9]+\.[0-9]+\\s+falling\\s+$sim0 4" "${lines[1]}"
+       regex_matches "[0-9]+\.[0-9]+\\s+rising\\s+$sim0 4" "${lines[2]}"
+       regex_matches "[0-9]+\.[0-9]+\\s+falling\\s+$sim0 4" "${lines[3]}"
+       num_lines_is 4
+}
+
+test_gpiomon_with_debounce_period() {
+       gpiosim_chip sim0 num_lines=4 line_name=1:foo line_name=2:bar
+       gpiosim_chip sim1 num_lines=8 line_name=3:baz line_name=4:xyz
+
+       dut_run gpiomon --banner --debounce-period 123us foo baz
+
+       run_tool gpioinfo
+
+       output_regex_match "\\s+line\\s+0:\\s+unnamed\\s+input"
+       output_regex_match \
+"\\s+line\\s+1:\\s+\"foo\"\\s+input edges=both debounce-period=123us"
+       output_regex_match \
+"\\s+line\\s+3:\\s+\"baz\"\\s+input edges=both debounce-period=123us"
+       status_is 0
+}
+
+test_gpiomon_with_idle_timeout() {
+       gpiosim_chip sim0 num_lines=8
+
+       local sim0=${GPIOSIM_CHIP_NAME[sim0]}
+
+       # redirect, as gpiomon exits
+       dut_run_redirect gpiomon --idle-timeout 10ms --chip $sim0 4
+
+       dut_wait
+       status_is 0
+       dut_read_redirect
+       num_lines_is 0
+}
+
+test_gpiomon_multiple_lines() {
+       gpiosim_chip sim0 num_lines=8
+
+       local sim0=${GPIOSIM_CHIP_NAME[sim0]}
+
+       dut_run gpiomon --banner --format=%o --chip $sim0 1 3 2 5 4
+       dut_regex_match "Monitoring lines .*"
+
+       gpiosim_set_pull sim0 2 pull-up
+       dut_regex_match "2"
+       gpiosim_set_pull sim0 3 pull-up
+       dut_regex_match "3"
+       gpiosim_set_pull sim0 4 pull-up
+       dut_regex_match "4"
+
+       assert_fail dut_readable
+}
+
+test_gpiomon_multiple_lines_by_name_and_offset() {
+       gpiosim_chip sim0 num_lines=4 line_name=1:foo line_name=2:bar
+
+       local sim0=${GPIOSIM_CHIP_NAME[sim0]}
+
+       dut_run gpiomon --banner --format=%o --chip $sim0 foo bar 3
+       dut_regex_match "Monitoring lines .*"
+
+       gpiosim_set_pull sim0 2 pull-up
+       dut_regex_match "2"
+       gpiosim_set_pull sim0 3 pull-up
+       dut_regex_match "3"
+       gpiosim_set_pull sim0 1 pull-up
+       dut_regex_match "1"
+
+       assert_fail dut_readable
+}
+
+test_gpiomon_multiple_lines_across_multiple_chips() {
+       gpiosim_chip sim0 num_lines=4 line_name=1:foo line_name=2:bar
+       gpiosim_chip sim1 num_lines=8 line_name=0:baz line_name=4:xyz
+
+       dut_run gpiomon --banner --format=%l foo bar baz
+       dut_regex_match "Monitoring lines .*"
+
+       gpiosim_set_pull sim0 2 pull-up
+       dut_regex_match "bar"
+       gpiosim_set_pull sim1 0 pull-up
+       dut_regex_match "baz"
+       gpiosim_set_pull sim0 1 pull-up
+       dut_regex_match "foo"
+
+       assert_fail dut_readable
+}
+
+test_gpiomon_exit_after_SIGINT() {
+       gpiosim_chip sim0 num_lines=8
+
+       local sim0=${GPIOSIM_CHIP_NAME[sim0]}
+
+       dut_run gpiomon --banner --chip $sim0 4
+       dut_regex_match "Monitoring line .*"
+
+       dut_kill -SIGINT
+       dut_wait
+
+       status_is 130
+}
+
+test_gpiomon_exit_after_SIGTERM() {
+       gpiosim_chip sim0 num_lines=8
+
+       local sim0=${GPIOSIM_CHIP_NAME[sim0]}
+
+       dut_run gpiomon --banner --chip $sim0 4
+       dut_regex_match "Monitoring line .*"
+
+       dut_kill -SIGTERM
+       dut_wait
+
+       status_is 143
+}
+
+test_gpiomon_with_nonexistent_line() {
+       run_tool gpiomon nonexistent-line
+
+       status_is 1
+       output_regex_match ".*cannot find line 'nonexistent-line'"
+}
+
+test_gpiomon_with_same_line_twice() {
+       gpiosim_chip sim0 num_lines=8 line_name=1:foo
+
+       local sim0=${GPIOSIM_CHIP_NAME[sim0]}
+
+       # by offset
+       run_tool gpiomon --chip $sim0 0 0
+
+       output_regex_match ".*lines '0' and '0' are the same line"
+       status_is 1
+
+       # by name
+       run_tool gpiomon foo foo
+
+       output_regex_match ".*lines 'foo' and 'foo' are the same line"
+       status_is 1
+
+       # by name and offset
+       run_tool gpiomon --chip $sim0 1 foo
+
+       output_regex_match ".*lines '1' and 'foo' are the same line"
+       status_is 1
+}
+
+test_gpiomon_with_strict_named_line_check() {
+       gpiosim_chip sim0 num_lines=4 line_name=1:foo line_name=2:bar \
+                                     line_name=3:foobar
+       gpiosim_chip sim1 num_lines=8 line_name=0:baz line_name=2:foobar \
+                                     line_name=4:xyz line_name=7:foobar
+       gpiosim_chip sim2 num_lines=16
+
+       run_tool gpiomon --strict foobar
+
+       output_regex_match ".*line 'foobar' is not unique"
+       status_is 1
+}
+test_gpiomon_with_lines_by_offset() {
+       # not suggesting this setup makes any sense
+       # - just test that we can deal with it
+       gpiosim_chip sim0 num_lines=8 line_name=1:6 line_name=6:1
+
+       local sim0=${GPIOSIM_CHIP_NAME[sim0]}
+
+       gpiosim_set_pull sim0 1 pull-up
+
+       dut_run gpiomon --banner --chip $sim0 6 1
+       dut_flush
+
+       gpiosim_set_pull sim0 1 pull-down
+       dut_regex_match "[0-9]+\.[0-9]+\\s+falling\\s+$sim0 1"
+
+       gpiosim_set_pull sim0 1 pull-up
+       dut_regex_match "[0-9]+\.[0-9]+\\s+rising\\s+$sim0 1"
+
+       gpiosim_set_pull sim0 6 pull-up
+       dut_regex_match "[0-9]+\.[0-9]+\\s+rising\\s+$sim0 6"
+
+       gpiosim_set_pull sim0 6 pull-down
+       dut_regex_match "[0-9]+\.[0-9]+\\s+falling\\s+$sim0 6"
+
+       assert_fail dut_readable
+}
+
+test_gpiomon_with_lines_strictly_by_name() {
+       # not suggesting this setup makes sense
+       # - just test that we can deal with it
+       gpiosim_chip sim0 num_lines=8 line_name=1:42 line_name=6:13
+
+       local sim0=${GPIOSIM_CHIP_NAME[sim0]}
+
+       gpiosim_set_pull sim0 1 pull-up
+
+       dut_run gpiomon --banner --by-name --chip $sim0 42 13
+       dut_flush
+
+       gpiosim_set_pull sim0 1 pull-down
+       dut_regex_match "[0-9]+\.[0-9]+\\s+falling\\s+$sim0 1"
+
+       gpiosim_set_pull sim0 1 pull-up
+       dut_regex_match "[0-9]+\.[0-9]+\\s+rising\\s+$sim0 1"
+
+       gpiosim_set_pull sim0 6 pull-up
+       dut_regex_match "[0-9]+\.[0-9]+\\s+rising\\s+$sim0 6"
+
+       gpiosim_set_pull sim0 6 pull-down
+       dut_regex_match "[0-9]+\.[0-9]+\\s+falling\\s+$sim0 6"
+
+       assert_fail dut_readable
+}
+
+test_gpiomon_with_no_arguments() {
+       run_tool gpiomon
+
+       output_regex_match ".*at least one GPIO line must be specified"
+       status_is 1
+}
+
+test_gpiomon_with_no_line_specified() {
+       gpiosim_chip sim0 num_lines=8
+
+       run_tool gpiomon --chip ${GPIOSIM_CHIP_NAME[sim0]}
+
+       output_regex_match ".*at least one GPIO line must be specified"
+       status_is 1
+}
+
+test_gpiomon_with_offset_out_of_range() {
+       gpiosim_chip sim0 num_lines=4
+
+       local sim0=${GPIOSIM_CHIP_NAME[sim0]}
+
+       run_tool gpiomon --chip $sim0 5
+
+       output_regex_match ".*offset 5 is out of range on chip '$sim0'"
+       status_is 1
+}
+
+test_gpiomon_with_invalid_bias() {
+       gpiosim_chip sim0 num_lines=8
+
+       local sim0=${GPIOSIM_CHIP_NAME[sim0]}
+
+       run_tool gpiomon --bias=bad -c $sim0 0 1
+
+       output_regex_match ".*invalid bias.*"
+       status_is 1
+}
+
+test_gpiomon_with_invalid_debounce_period() {
+       gpiosim_chip sim0 num_lines=8
+
+       local sim0=${GPIOSIM_CHIP_NAME[sim0]}
+
+       run_tool gpiomon --debounce-period bad -c $sim0 0 1
+
+       output_regex_match ".*invalid period: bad"
+       status_is 1
+}
+
+test_gpiomon_with_invalid_idle_timeout() {
+       gpiosim_chip sim0 num_lines=8
+
+       local sim0=${GPIOSIM_CHIP_NAME[sim0]}
+
+       run_tool gpiomon --idle-timeout bad -c $sim0 0 1
+
+       output_regex_match ".*invalid period: bad"
+       status_is 1
+}
+
+test_gpiomon_with_custom_format_event_type_offset() {
+       gpiosim_chip sim0 num_lines=8
+
+       local sim0=${GPIOSIM_CHIP_NAME[sim0]}
+
+       dut_run gpiomon --banner "--format=%e %o" -c $sim0 4
+       dut_flush
+
+       gpiosim_set_pull sim0 4 pull-up
+       dut_read
+       output_is "1 4"
+}
+
+test_gpiomon_with_custom_format_event_type_offset_joined() {
+       gpiosim_chip sim0 num_lines=8
+
+       local sim0=${GPIOSIM_CHIP_NAME[sim0]}
+
+       dut_run gpiomon --banner "--format=%e%o" -c $sim0 4
+       dut_flush
+
+       gpiosim_set_pull sim0 4 pull-up
+       dut_read
+       output_is "14"
+}
+
+test_gpiomon_with_custom_format_edge_chip_and_line() {
+       gpiosim_chip sim0 num_lines=8 line_name=4:baz
+
+       local sim0=${GPIOSIM_CHIP_NAME[sim0]}
+
+       dut_run gpiomon --banner "--format=%e %o %E %c %l" -c $sim0 baz
+       dut_flush
+
+       gpiosim_set_pull sim0 4 pull-up
+       dut_regex_match "1 4 rising $sim0 baz"
+}
+
+test_gpiomon_with_custom_format_seconds_timestamp() {
+       gpiosim_chip sim0 num_lines=8
+
+       local sim0=${GPIOSIM_CHIP_NAME[sim0]}
+
+       dut_run gpiomon --banner "--format=%e %o %S" -c $sim0 4
+       dut_flush
+
+       gpiosim_set_pull sim0 4 pull-up
+       dut_regex_match "1 4 [0-9]+\\.[0-9]+"
+}
+
+test_gpiomon_with_custom_format_UTC_timestamp() {
+       gpiosim_chip sim0 num_lines=8
+
+       local sim0=${GPIOSIM_CHIP_NAME[sim0]}
+
+       dut_run gpiomon --banner "--format=%U %e %o " --event-clock=realtime \
+               -c $sim0 4
+       dut_flush
+
+       gpiosim_set_pull sim0 4 pull-up
+       dut_regex_match \
+"[0-9][0-9][0-9][0-9]-[0-1][0-9]-[0-3][0-9]T[0-2][0-9]:[0-5][0-9]:[0-5][0-9]\\.[0-9]+Z 1 4"
+}
+
+test_gpiomon_with_custom_format_localtime_timestamp() {
+       gpiosim_chip sim0 num_lines=8
+
+       local sim0=${GPIOSIM_CHIP_NAME[sim0]}
+
+       dut_run gpiomon --banner "--format=%L %e %o" --event-clock=realtime \
+               -c $sim0 4
+       dut_flush
+
+       gpiosim_set_pull sim0 4 pull-up
+       dut_regex_match \
+"[0-9][0-9][0-9][0-9]-[0-1][0-9]-[0-3][0-9]T[0-2][0-9]:[0-5][0-9]:[0-5][0-9]\\.[0-9]+ 1 4"
+}
+
+test_gpiomon_with_custom_format_double_percent_sign() {
+       gpiosim_chip sim0 num_lines=8
+
+       local sim0=${GPIOSIM_CHIP_NAME[sim0]}
+
+       dut_run gpiomon --banner "--format=start%%end" -c $sim0 4
+       dut_flush
+
+       gpiosim_set_pull sim0 4 pull-up
+       dut_read
+       output_is "start%end"
+}
+
+test_gpiomon_with_custom_format_double_percent_sign_event_type_specifier() {
+       gpiosim_chip sim0 num_lines=8
+
+       local sim0=${GPIOSIM_CHIP_NAME[sim0]}
+
+       dut_run gpiomon --banner "--format=%%e" -c $sim0 4
+       dut_flush
+
+       gpiosim_set_pull sim0 4 pull-up
+       dut_read
+       output_is "%e"
+}
+
+test_gpiomon_with_custom_format_single_percent_sign() {
+       gpiosim_chip sim0 num_lines=8
+
+       local sim0=${GPIOSIM_CHIP_NAME[sim0]}
+
+       dut_run gpiomon --banner "--format=%" -c $sim0 4
+       dut_flush
+
+       gpiosim_set_pull sim0 4 pull-up
+       dut_read
+       output_is "%"
+}
+
+test_gpiomon_with_custom_format_single_percent_sign_between_other_characters() {
+       gpiosim_chip sim0 num_lines=8
+
+       local sim0=${GPIOSIM_CHIP_NAME[sim0]}
+
+       dut_run gpiomon --banner "--format=foo % bar" -c $sim0 4
+       dut_flush
+
+       gpiosim_set_pull sim0 4 pull-up
+       dut_read
+       output_is "foo % bar"
+}
+
+test_gpiomon_with_custom_format_unknown_specifier() {
+       gpiosim_chip sim0 num_lines=8
+
+       local sim0=${GPIOSIM_CHIP_NAME[sim0]}
+
+       dut_run gpiomon --banner "--format=%x" -c $sim0 4
+       dut_flush
+
+       gpiosim_set_pull sim0 4 pull-up
+       dut_read
+       output_is "%x"
+}
+
+#
+# gpionotify test cases
+#
+
+test_gpionotify_by_name() {
+       gpiosim_chip sim0 num_lines=8 line_name=4:foo
+
+       local sim0=${GPIOSIM_CHIP_NAME[sim0]}
+
+       dut_run gpionotify --banner foo
+       dut_regex_match "Watching line .*"
+
+       request_release_line $sim0 4
+
+       dut_regex_match "[0-9]+\.[0-9]+\\s+requested\\s+\"foo\""
+       dut_regex_match "[0-9]+\.[0-9]+\\s+released\\s+\"foo\""
+       # tools currently have no way to generate a reconfig event
+}
+
+test_gpionotify_by_offset() {
+       gpiosim_chip sim0 num_lines=8
+
+       local sim0=${GPIOSIM_CHIP_NAME[sim0]}
+
+       dut_run gpionotify --banner --chip $sim0 4
+       dut_regex_match "Watching line .*"
+
+       request_release_line $sim0 4
+       dut_regex_match "[0-9]+\.[0-9]+\\s+requested\\s+$sim0 4"
+       dut_regex_match "[0-9]+\.[0-9]+\\s+released\\s+$sim0 4"
+
+       assert_fail dut_readable
+}
+
+test_gpionotify_by_symlink() {
+       gpiosim_chip sim0 num_lines=8
+       gpiosim_chip_symlink sim0 .
+
+       local sim0=${GPIOSIM_CHIP_NAME[sim0]}
+
+       dut_run gpionotify --banner --chip $GPIOSIM_CHIP_LINK 4
+       dut_regex_match "Watching line .*"
+
+       request_release_line $sim0 4
+       dut_regex_match "[0-9]+\.[0-9]+\\s+requested\\s+$sim0\\s+4"
+       dut_regex_match "[0-9]+\.[0-9]+\\s+released\\s+$sim0\\s+4"
+
+       assert_fail dut_readable
+}
+
+test_gpionotify_by_chip_and_name() {
+       gpiosim_chip sim0 num_lines=8 line_name=4:foo
+       gpiosim_chip sim1 num_lines=8 line_name=2:foo
+
+       local sim1=${GPIOSIM_CHIP_NAME[sim1]}
+
+       dut_run gpionotify --banner --chip $sim1 foo
+       dut_regex_match "Watching line .*"
+
+       request_release_line $sim1 2
+       dut_regex_match "[0-9]+\.[0-9]+\\s+requested\\s+$sim1 2 \"foo\""
+       dut_regex_match "[0-9]+\.[0-9]+\\s+released\\s+$sim1 2 \"foo\""
+
+       assert_fail dut_readable
+}
+
+test_gpionotify_first_matching_named_line() {
+       gpiosim_chip sim0 num_lines=4 line_name=1:foo line_name=2:bar \
+                                     line_name=3:foobar
+       gpiosim_chip sim1 num_lines=8 line_name=0:baz line_name=2:foobar \
+                                     line_name=4:xyz line_name=7:foobar
+       gpiosim_chip sim2 num_lines=16
+
+       dut_run gpionotify --banner foobar
+       dut_regex_match "Watching line .*"
+
+       request_release_line ${GPIOSIM_CHIP_NAME[sim0]} 3
+       dut_regex_match "[0-9]+\.[0-9]+\\s+requested\\s+\"foobar\""
+       dut_regex_match "[0-9]+\.[0-9]+\\s+released\\s+\"foobar\""
+
+       assert_fail dut_readable
+}
+
+test_gpionotify_with_requested() {
+       gpiosim_chip sim0 num_lines=8
+
+       local sim0=${GPIOSIM_CHIP_NAME[sim0]}
+
+       gpiosim_set_pull sim0 4 pull-up
+
+       dut_run gpionotify --banner --event=requested --chip $sim0 4
+       dut_flush
+
+       request_release_line ${GPIOSIM_CHIP_NAME[sim0]} 4
+       dut_regex_match "[0-9]+\.[0-9]+\\s+requested\\s+$sim0 4"
+       assert_fail dut_readable
+}
+
+test_gpionotify_with_released() {
+       gpiosim_chip sim0 num_lines=8
+       local sim0=${GPIOSIM_CHIP_NAME[sim0]}
+
+       gpiosim_set_pull sim0 4 pull-down
+
+       dut_run gpionotify --banner --event=released --chip $sim0 4
+       dut_flush
+
+       request_release_line ${GPIOSIM_CHIP_NAME[sim0]} 4
+       dut_regex_match "[0-9]+\.[0-9]+\\s+released\\s+$sim0 4"
+       assert_fail dut_readable
+}
+
+test_gpionotify_with_quiet_mode() {
+       gpiosim_chip sim0 num_lines=8
+
+       local sim0=${GPIOSIM_CHIP_NAME[sim0]}
+
+       dut_run gpionotify --banner --quiet --chip $sim0 4
+       dut_flush
+
+       request_release_line ${GPIOSIM_CHIP_NAME[sim0]} 4
+       assert_fail dut_readable
+}
+
+test_gpionotify_with_unquoted() {
+       gpiosim_chip sim0 num_lines=8 line_name=4:foo
+
+       local sim0=${GPIOSIM_CHIP_NAME[sim0]}
+
+       dut_run gpionotify --banner --unquoted foo
+       dut_regex_match "Watching line .*"
+
+       request_release_line $sim0 4
+
+       dut_regex_match "[0-9]+\.[0-9]+\\s+requested\\s+foo"
+       dut_regex_match "[0-9]+\.[0-9]+\\s+released\\s+foo"
+}
+
+test_gpionotify_with_num_events() {
+       gpiosim_chip sim0 num_lines=8
+
+       local sim0=${GPIOSIM_CHIP_NAME[sim0]}
+
+       # redirect, as gpionotify exits after 4 events
+       dut_run_redirect gpionotify --num-events=4 --chip $sim0 3 4
+
+
+       request_release_line ${GPIOSIM_CHIP_NAME[sim0]} 4
+       request_release_line ${GPIOSIM_CHIP_NAME[sim0]} 3
+
+       dut_wait
+       status_is 0
+       dut_read_redirect
+
+       regex_matches "[0-9]+\.[0-9]+\\s+requested\\s+$sim0 4" "${lines[0]}"
+       regex_matches "[0-9]+\.[0-9]+\\s+released\\s+$sim0 4" "${lines[1]}"
+       regex_matches "[0-9]+\.[0-9]+\\s+requested\\s+$sim0 3" "${lines[2]}"
+       regex_matches "[0-9]+\.[0-9]+\\s+released\\s+$sim0 3" "${lines[3]}"
+       num_lines_is 4
+}
+
+test_gpionotify_with_idle_timeout() {
+       gpiosim_chip sim0 num_lines=8
+
+       local sim0=${GPIOSIM_CHIP_NAME[sim0]}
+
+       # redirect, as gpionotify exits
+       dut_run_redirect gpionotify --idle-timeout 10ms --chip $sim0 3 4
+
+       dut_wait
+       status_is 0
+       dut_read_redirect
+
+       num_lines_is 0
+}
+
+test_gpionotify_multiple_lines() {
+       gpiosim_chip sim0 num_lines=8
+
+       local sim0=${GPIOSIM_CHIP_NAME[sim0]}
+
+       dut_run gpionotify --banner --chip $sim0 1 2 3 4 5
+       dut_regex_match "Watching lines .*"
+
+       request_release_line $sim0 2
+       dut_regex_match "[0-9]+\.[0-9]+\\s+requested\\s+$sim0 2"
+       dut_regex_match "[0-9]+\.[0-9]+\\s+released\\s+$sim0 2"
+
+       request_release_line $sim0 3
+       dut_regex_match "[0-9]+\.[0-9]+\\s+requested\\s+$sim0 3"
+       dut_regex_match "[0-9]+\.[0-9]+\\s+released\\s+$sim0 3"
+
+       request_release_line $sim0 4
+       dut_regex_match "[0-9]+\.[0-9]+\\s+requested\\s+$sim0 4"
+       dut_regex_match "[0-9]+\.[0-9]+\\s+released\\s+$sim0 4"
+
+       assert_fail dut_readable
+}
+
+test_gpionotify_multiple_lines_by_name_and_offset() {
+       gpiosim_chip sim0 num_lines=4 line_name=1:foo line_name=2:bar
+
+       local sim0=${GPIOSIM_CHIP_NAME[sim0]}
+
+       dut_run gpionotify --banner --chip $sim0 bar foo 3
+       dut_regex_match "Watching lines .*"
+
+       request_release_line $sim0 2
+       dut_regex_match "[0-9]+\.[0-9]+\\s+requested\\s+$sim0 2\\s+\"bar\""
+       dut_regex_match "[0-9]+\.[0-9]+\\s+released\\s+$sim0 2\\s+\"bar\""
+
+       request_release_line $sim0 1
+       dut_regex_match "[0-9]+\.[0-9]+\\s+requested\\s+$sim0 1\\s+\"foo\""
+       dut_regex_match "[0-9]+\.[0-9]+\\s+released\\s+$sim0 1\\s+\"foo\""
+
+       request_release_line $sim0 3
+       dut_regex_match "[0-9]+\.[0-9]+\\s+requested\\s+$sim0 3"
+       dut_regex_match "[0-9]+\.[0-9]+\\s+released\\s+$sim0 3"
+
+       assert_fail dut_readable
+}
+
+test_gpionotify_multiple_lines_across_multiple_chips() {
+       gpiosim_chip sim0 num_lines=4 line_name=1:foo line_name=2:bar
+       gpiosim_chip sim1 num_lines=8 line_name=0:baz line_name=4:xyz
+
+       local sim0=${GPIOSIM_CHIP_NAME[sim0]}
+       local sim1=${GPIOSIM_CHIP_NAME[sim1]}
+
+       dut_run gpionotify --banner baz bar foo xyz
+       dut_regex_match "Watching lines .*"
+
+       request_release_line $sim0 2
+       dut_regex_match "[0-9]+\.[0-9]+\\s+requested\\s+\"bar\""
+       dut_regex_match "[0-9]+\.[0-9]+\\s+released\\s+\"bar\""
+
+       request_release_line $sim0 1
+       dut_regex_match "[0-9]+\.[0-9]+\\s+requested\\s+\"foo\""
+       dut_regex_match "[0-9]+\.[0-9]+\\s+released\\s+\"foo\""
+
+       request_release_line $sim1 4
+       dut_regex_match "[0-9]+\.[0-9]+\\s+requested\\s+\"xyz\""
+       dut_regex_match "[0-9]+\.[0-9]+\\s+released\\s+\"xyz\""
+
+       request_release_line $sim1 0
+       dut_regex_match "[0-9]+\.[0-9]+\\s+requested\\s+\"baz\""
+       dut_regex_match "[0-9]+\.[0-9]+\\s+released\\s+\"baz\""
+
+       assert_fail dut_readable
+}
+
+test_gpionotify_exit_after_SIGINT() {
+       gpiosim_chip sim0 num_lines=8
+
+       dut_run gpionotify --banner --chip ${GPIOSIM_CHIP_NAME[sim0]} 4
+       dut_regex_match "Watching line .*"
+
+       dut_kill -SIGINT
+       dut_wait
+
+       status_is 130
+}
+
+test_gpionotify_exit_after_SIGTERM() {
+       gpiosim_chip sim0 num_lines=8
+
+       dut_run gpionotify --banner --chip ${GPIOSIM_CHIP_NAME[sim0]} 4
+       dut_regex_match "Watching line .*"
+
+       dut_kill -SIGTERM
+       dut_wait
+
+       status_is 143
+}
+
+test_gpionotify_with_nonexistent_line() {
+       run_tool gpionotify nonexistent-line
+
+       status_is 1
+       output_regex_match ".*cannot find line 'nonexistent-line'"
+}
+
+test_gpionotify_with_same_line_twice() {
+       gpiosim_chip sim0 num_lines=8 line_name=1:foo
+
+       local sim0=${GPIOSIM_CHIP_NAME[sim0]}
+
+       # by offset
+       run_tool gpionotify --chip $sim0 0 0
+
+       output_regex_match ".*lines '0' and '0' are the same line"
+       num_lines_is 1
+       status_is 1
+
+       # by name
+       run_tool gpionotify foo foo
+
+       output_regex_match ".*lines 'foo' and 'foo' are the same line"
+       num_lines_is 1
+       status_is 1
+
+       # by name and offset
+       run_tool gpionotify --chip $sim0 1 foo
+
+       output_regex_match ".*lines '1' and 'foo' are the same line"
+       num_lines_is 1
+       status_is 1
+}
+
+test_gpionotify_with_strict_named_line_check() {
+       gpiosim_chip sim0 num_lines=4 line_name=1:foo line_name=2:bar \
+                                     line_name=3:foobar
+       gpiosim_chip sim1 num_lines=8 line_name=0:baz line_name=2:foobar \
+                                     line_name=4:xyz line_name=7:foobar
+       gpiosim_chip sim2 num_lines=16
+
+       run_tool gpionotify --strict foobar
+
+       output_regex_match ".*line 'foobar' is not unique"
+       status_is 1
+}
+
+test_gpionotify_with_lines_by_offset() {
+       # not suggesting this setup makes any sense
+       # - just test that we can deal with it
+       gpiosim_chip sim0 num_lines=8 line_name=1:6 line_name=6:1
+
+       local sim0=${GPIOSIM_CHIP_NAME[sim0]}
+
+       dut_run gpionotify --banner --chip $sim0 1
+       dut_flush
+
+       request_release_line $sim0 1
+       dut_regex_match "[0-9]+\.[0-9]+\\s+requested\\s+$sim0 1"
+       dut_regex_match "[0-9]+\.[0-9]+\\s+released\\s+$sim0 1"
+
+       request_release_line $sim0 6
+
+       assert_fail dut_readable
+}
+
+test_gpionotify_with_lines_strictly_by_name() {
+       # not suggesting this setup makes any sense
+       # - just test that we can deal with it
+       gpiosim_chip sim0 num_lines=8 line_name=1:6 line_name=6:1
+
+       local sim0=${GPIOSIM_CHIP_NAME[sim0]}
+
+       dut_run gpionotify --banner --by-name --chip $sim0 1
+       dut_flush
+
+       request_release_line $sim0 6
+       dut_regex_match "[0-9]+\.[0-9]+\\s+requested\\s+$sim0 6 \"1\""
+       dut_regex_match "[0-9]+\.[0-9]+\\s+released\\s+$sim0 6 \"1\""
+
+       request_release_line $sim0 1
+       assert_fail dut_readable
+}
+
+test_gpionotify_with_no_arguments() {
+       run_tool gpionotify
+
+       output_regex_match ".*at least one GPIO line must be specified"
+       status_is 1
+}
+
+test_gpionotify_with_no_line_specified() {
+       gpiosim_chip sim0 num_lines=8
+
+       run_tool gpionotify --chip ${GPIOSIM_CHIP_NAME[sim0]}
+
+       output_regex_match ".*at least one GPIO line must be specified"
+       status_is 1
+}
+
+test_gpionotify_with_offset_out_of_range() {
+       gpiosim_chip sim0 num_lines=4
+
+       local sim0=${GPIOSIM_CHIP_NAME[sim0]}
+
+       run_tool gpionotify --chip $sim0 5
+
+       output_regex_match ".*offset 5 is out of range on chip '$sim0'"
+       status_is 1
+}
+
+test_gpionotify_with_invalid_idle_timeout() {
+       gpiosim_chip sim0 num_lines=8
+
+       local sim0=${GPIOSIM_CHIP_NAME[sim0]}
+
+       run_tool gpionotify --idle-timeout bad -c $sim0 0 1
+
+       output_regex_match ".*invalid period: bad"
+       status_is 1
+}
+
+test_gpionotify_with_custom_format_event_type_offset() {
+       gpiosim_chip sim0 num_lines=8
+
+       local sim0=${GPIOSIM_CHIP_NAME[sim0]}
+
+       dut_run gpionotify --banner --event=requested "--format=%e %o" -c $sim0 4
+       dut_flush
+
+       request_release_line $sim0 4
+       dut_read
+       output_is "1 4"
+}
+
+test_gpionotify_with_custom_format_event_type_offset_joined() {
+       gpiosim_chip sim0 num_lines=8
+
+       local sim0=${GPIOSIM_CHIP_NAME[sim0]}
+
+       dut_run gpionotify --banner --event=requested "--format=%e%o" -c $sim0 4
+       dut_flush
+
+       request_release_line $sim0 4
+       dut_read
+       output_is "14"
+}
+
+test_gpionotify_with_custom_format_event_chip_and_line() {
+       gpiosim_chip sim0 num_lines=8 line_name=4:baz
+
+       local sim0=${GPIOSIM_CHIP_NAME[sim0]}
+
+       dut_run gpionotify --banner --event=released \
+               "--format=%e %o %E %c %l" -c $sim0 baz
+       dut_flush
+
+       request_release_line $sim0 4
+       dut_regex_match "2 4 released $sim0 baz"
+}
+
+test_gpionotify_with_custom_format_seconds_timestamp() {
+       gpiosim_chip sim0 num_lines=8
+
+       local sim0=${GPIOSIM_CHIP_NAME[sim0]}
+
+       dut_run gpionotify --banner --event=requested "--format=%e %o %S" \
+               -c $sim0 4
+       dut_flush
+
+       request_release_line $sim0 4
+       dut_regex_match "1 4 [0-9]+\\.[0-9]+"
+}
+
+test_gpionotify_with_custom_format_UTC_timestamp() {
+       gpiosim_chip sim0 num_lines=8
+
+       local sim0=${GPIOSIM_CHIP_NAME[sim0]}
+
+       dut_run gpionotify --banner --event=released \
+               "--format=%U %e %o" -c $sim0 4
+       dut_flush
+
+       request_release_line $sim0 4
+       dut_regex_match \
+"[0-9][0-9][0-9][0-9]-[0-1][0-9]-[0-3][0-9]T[0-2][0-9]:[0-5][0-9]:[0-5][0-9]\\.[0-9]+Z 2 4"
+}
+
+test_gpionotify_with_custom_format_localtime_timestamp() {
+       gpiosim_chip sim0 num_lines=8
+
+       local sim0=${GPIOSIM_CHIP_NAME[sim0]}
+
+       dut_run gpionotify --banner --event=released \
+               "--format=%L %e %o" -c $sim0 4
+       dut_flush
+
+       request_release_line $sim0 4
+       dut_regex_match \
+"[0-9][0-9][0-9][0-9]-[0-1][0-9]-[0-3][0-9]T[0-2][0-9]:[0-5][0-9]:[0-5][0-9]\\.[0-9]+ 2 4"
+}
+
+test_gpionotify_with_custom_format_double_percent_sign() {
+       gpiosim_chip sim0 num_lines=8
+
+       local sim0=${GPIOSIM_CHIP_NAME[sim0]}
+
+       dut_run gpionotify --banner --event=requested "--format=start%%end" \
+               -c $sim0 4
+       dut_flush
+
+       request_release_line $sim0 4
+       dut_read
+       output_is "start%end"
+}
+
+test_gpionotify_with_custom_format_double_percent_sign_event_type_specifier() {
+       gpiosim_chip sim0 num_lines=8
+
+       local sim0=${GPIOSIM_CHIP_NAME[sim0]}
+
+       dut_run gpionotify --banner --event=requested "--format=%%e" -c $sim0 4
+       dut_flush
+
+       request_release_line $sim0 4
+       dut_read
+       output_is "%e"
+}
+
+test_gpionotify_with_custom_format_single_percent_sign() {
+       gpiosim_chip sim0 num_lines=8
+
+       local sim0=${GPIOSIM_CHIP_NAME[sim0]}
+
+       dut_run gpionotify --banner --event=requested "--format=%" -c $sim0 4
+       dut_flush
+
+       request_release_line $sim0 4
+       dut_read
+       output_is "%"
+}
+
+test_gpionotify_with_custom_format_single_percent_sign_between_other_characters() {
+       gpiosim_chip sim0 num_lines=8
+
+       local sim0=${GPIOSIM_CHIP_NAME[sim0]}
+
+       dut_run gpionotify --banner --event=requested "--format=foo % bar" -c $sim0 4
+       dut_flush
+
+       request_release_line $sim0 4
+       dut_read
+       output_is "foo % bar"
+}
+
+test_gpionotify_with_custom_format_unknown_specifier() {
+       gpiosim_chip sim0 num_lines=8
+
+       local sim0=${GPIOSIM_CHIP_NAME[sim0]}
+
+       dut_run gpionotify --banner --event=requested "--format=%x" -c $sim0 4
+       dut_flush
+
+       request_release_line $sim0 4
+       dut_read
+       output_is "%x"
+}
+
+die() {
+       echo "$@" 1>&2
+       exit 1
+}
+
+# Must be done after we sources shunit2 as we need SHUNIT_VERSION to be set.
+oneTimeSetUp() {
+       test "$SHUNIT_VERSION" = "$MIN_SHUNIT_VERSION" && return 0
+       local FIRST=$(printf "$SHUNIT_VERSION\n$MIN_SHUNIT_VERSION\n" | sort -Vr | head -1)
+       test "$FIRST" = "$MIN_SHUNIT_VERSION" && \
+               die "minimum shunit version required is $MIN_SHUNIT_VERSION (current version is $SHUNIT_VERSION"
+}
+
+check_kernel() {
+       local REQUIRED=$1
+       local CURRENT=$(uname -r)
+
+       SORTED=$(printf "$REQUIRED\n$CURRENT" | sort -V | head -n 1)
+
+       if [ "$SORTED" != "$REQUIRED" ]
+       then
+               die "linux kernel version must be at least: v$REQUIRED - got: v$CURRENT"
+       fi
+}
+
+check_prog() {
+       local PROG=$1
+
+       which "$PROG" > /dev/null
+       if [ "$?" -ne "0" ]
+       then
+               die "$PROG not found - needed to run the tests"
+       fi
+}
+
+# Check all required non-coreutils tools
+check_prog shunit2
+check_prog modprobe
+check_prog timeout
+check_prog grep
+
+# Check if we're running a kernel at the required version or later
+check_kernel $MIN_KERNEL_VERSION
+
+modprobe gpio-sim || die "unable to load the gpio-sim module"
+mountpoint /sys/kernel/config/ > /dev/null 2> /dev/null || \
+       die "configfs not mounted at /sys/kernel/config/"
+
+. shunit2
diff --git a/tools/gpiodetect.c b/tools/gpiodetect.c
new file mode 100644 (file)
index 0000000..0a3461b
--- /dev/null
@@ -0,0 +1,124 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+// SPDX-FileCopyrightText: 2017-2021 Bartosz Golaszewski <bartekgola@gmail.com>
+// SPDX-FileCopyrightText: 2022 Kent Gibson <warthog618@gmail.com>
+
+#include <getopt.h>
+#include <gpiod.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "tools-common.h"
+
+static void print_help(void)
+{
+       printf("Usage: %s [OPTIONS] [chip]...\n", get_prog_name());
+       printf("\n");
+       printf("List GPIO chips, print their labels and number of GPIO lines.\n");
+       printf("\n");
+       printf("Chips may be identified by number, name, or path.\n");
+       printf("e.g. '0', 'gpiochip0', and '/dev/gpiochip0' all refer to the same chip.\n");
+       printf("\n");
+       printf("If no chips are specified then all chips are listed.\n");
+       printf("\n");
+       printf("Options:\n");
+       printf("  -h, --help\t\tdisplay this help and exit\n");
+       printf("  -v, --version\t\toutput version information and exit\n");
+}
+
+static int parse_config(int argc, char **argv)
+{
+       static const struct option longopts[] = {
+               { "help",       no_argument,    NULL,   'h' },
+               { "version",    no_argument,    NULL,   'v' },
+               { GETOPT_NULL_LONGOPT },
+       };
+
+       static const char *const shortopts = "+hv";
+
+       int optc, opti;
+
+       for (;;) {
+               optc = getopt_long(argc, argv, shortopts, longopts, &opti);
+               if (optc < 0)
+                       break;
+
+               switch (optc) {
+               case 'h':
+                       print_help();
+                       exit(EXIT_SUCCESS);
+               case 'v':
+                       print_version();
+                       exit(EXIT_SUCCESS);
+               case '?':
+                       die("try %s --help", get_prog_name());
+               default:
+                       abort();
+               }
+       }
+       return optind;
+}
+
+static int print_chip_info(const char *path)
+{
+       struct gpiod_chip_info *info;
+       struct gpiod_chip *chip;
+
+       chip = gpiod_chip_open(path);
+       if (!chip) {
+               print_perror("unable to open chip '%s'", path);
+               return 1;
+       }
+
+       info = gpiod_chip_get_info(chip);
+       if (!info)
+               die_perror("unable to read info for '%s'", path);
+
+       printf("%s [%s] (%zu lines)\n", gpiod_chip_info_get_name(info),
+              gpiod_chip_info_get_label(info),
+              gpiod_chip_info_get_num_lines(info));
+
+       gpiod_chip_info_free(info);
+       gpiod_chip_close(chip);
+
+       return 0;
+}
+
+int main(int argc, char **argv)
+{
+       int num_chips, i, ret = EXIT_SUCCESS;
+       char **paths, *path;
+
+       set_prog_name(argv[0]);
+       i = parse_config(argc, argv);
+       argc -= i;
+       argv += i;
+
+       if (argc == 0) {
+               num_chips = all_chip_paths(&paths);
+               for (i = 0; i < num_chips; i++) {
+                       if (print_chip_info(paths[i]))
+                               ret = EXIT_FAILURE;
+
+                       free(paths[i]);
+               }
+
+               free(paths);
+       }
+
+       for (i = 0; i < argc; i++) {
+               if (chip_path_lookup(argv[i], &path)) {
+                       if (print_chip_info(path))
+                               ret = EXIT_FAILURE;
+
+                       free(path);
+               } else {
+                       print_error(
+                               "cannot find GPIO chip character device '%s'",
+                               argv[i]);
+                       ret = EXIT_FAILURE;
+               }
+       }
+
+       return ret;
+}
diff --git a/tools/gpioget.c b/tools/gpioget.c
new file mode 100644 (file)
index 0000000..f611737
--- /dev/null
@@ -0,0 +1,243 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+// SPDX-FileCopyrightText: 2017-2021 Bartosz Golaszewski <bartekgola@gmail.com>
+// SPDX-FileCopyrightText: 2022 Kent Gibson <warthog618@gmail.com>
+
+#include <getopt.h>
+#include <gpiod.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+#include "tools-common.h"
+
+struct config {
+       bool active_low;
+       bool by_name;
+       bool numeric;
+       bool strict;
+       bool unquoted;
+       enum gpiod_line_bias bias;
+       enum gpiod_line_direction direction;
+       unsigned int hold_period_us;
+       const char *chip_id;
+       const char *consumer;
+};
+
+static void print_help(void)
+{
+       printf("Usage: %s [OPTIONS] <line>...\n", get_prog_name());
+       printf("\n");
+       printf("Read values of GPIO lines.\n");
+       printf("\n");
+       printf("Lines are specified by name, or optionally by offset if the chip option\n");
+       printf("is provided.\n");
+       printf("\n");
+       printf("Options:\n");
+       printf("  -a, --as-is\t\tleave the line direction unchanged, not forced to input\n");
+       print_bias_help();
+       printf("      --by-name\t\ttreat lines as names even if they would parse as an offset\n");
+       printf("  -c, --chip <chip>\trestrict scope to a particular chip\n");
+       printf("  -C, --consumer <name>\tconsumer name applied to requested lines (default is 'gpioget')\n");
+       printf("  -h, --help\t\tdisplay this help and exit\n");
+       printf("  -l, --active-low\ttreat the line as active low\n");
+       printf("  -p, --hold-period <period>\n");
+       printf("\t\t\twait between requesting the lines and reading the values\n");
+       printf("      --numeric\t\tdisplay line values as '0' (inactive) or '1' (active)\n");
+       printf("  -s, --strict\t\tabort if requested line names are not unique\n");
+       printf("      --unquoted\tdon't quote line names\n");
+       printf("  -v, --version\t\toutput version information and exit\n");
+       print_chip_help();
+       print_period_help();
+}
+
+static int parse_config(int argc, char **argv, struct config *cfg)
+{
+       static const struct option longopts[] = {
+               { "active-low", no_argument,            NULL,   'l' },
+               { "as-is",      no_argument,            NULL,   'a' },
+               { "bias",       required_argument,      NULL,   'b' },
+               { "by-name",    no_argument,            NULL,   'B' },
+               { "chip",       required_argument,      NULL,   'c' },
+               { "consumer",   required_argument,      NULL,   'C' },
+               { "help",       no_argument,            NULL,   'h' },
+               { "hold-period", required_argument,     NULL,   'p' },
+               { "numeric",    no_argument,            NULL,   'N' },
+               { "strict",     no_argument,            NULL,   's' },
+               { "unquoted",   no_argument,            NULL,   'Q' },
+               { "version",    no_argument,            NULL,   'v' },
+               { GETOPT_NULL_LONGOPT },
+       };
+
+       static const char *const shortopts = "+ab:c:C:hlp:sv";
+
+       int opti, optc;
+
+       memset(cfg, 0, sizeof(*cfg));
+       cfg->direction = GPIOD_LINE_DIRECTION_INPUT;
+       cfg->consumer = "gpioget";
+
+       for (;;) {
+               optc = getopt_long(argc, argv, shortopts, longopts, &opti);
+               if (optc < 0)
+                       break;
+
+               switch (optc) {
+               case 'a':
+                       cfg->direction = GPIOD_LINE_DIRECTION_AS_IS;
+                       break;
+               case 'b':
+                       cfg->bias = parse_bias_or_die(optarg);
+                       break;
+               case 'B':
+                       cfg->by_name = true;
+                       break;
+               case 'c':
+                       cfg->chip_id = optarg;
+                       break;
+               case 'C':
+                       cfg->consumer = optarg;
+                       break;
+               case 'l':
+                       cfg->active_low = true;
+                       break;
+               case 'N':
+                       cfg->numeric = true;
+                       break;
+               case 'p':
+                       cfg->hold_period_us = parse_period_or_die(optarg);
+                       break;
+               case 'Q':
+                       cfg->unquoted = true;
+                       break;
+               case 's':
+                       cfg->strict = true;
+                       break;
+               case 'h':
+                       print_help();
+                       exit(EXIT_SUCCESS);
+               case 'v':
+                       print_version();
+                       exit(EXIT_SUCCESS);
+               case '?':
+                       die("try %s --help", get_prog_name());
+               case 0:
+                       break;
+               default:
+                       abort();
+               }
+       }
+
+       return optind;
+}
+
+int main(int argc, char **argv)
+{
+       struct gpiod_line_settings *settings;
+       struct gpiod_request_config *req_cfg;
+       struct gpiod_line_request *request;
+       struct gpiod_line_config *line_cfg;
+       struct line_resolver *resolver;
+       enum gpiod_line_value *values;
+       struct resolved_line *line;
+       struct gpiod_chip *chip;
+       unsigned int *offsets;
+       int i, num_lines, ret;
+       struct config cfg;
+       const char *fmt;
+
+       set_prog_name(argv[0]);
+       i = parse_config(argc, argv, &cfg);
+       argc -= i;
+       argv += i;
+
+       if (argc < 1)
+               die("at least one GPIO line must be specified");
+
+       resolver = resolve_lines(argc, argv, cfg.chip_id, cfg.strict,
+                                cfg.by_name);
+       validate_resolution(resolver, cfg.chip_id);
+
+       offsets = calloc(resolver->num_lines, sizeof(*offsets));
+       values = calloc(resolver->num_lines, sizeof(*values));
+       if (!offsets || !values)
+               die("out of memory");
+
+       settings = gpiod_line_settings_new();
+       if (!settings)
+               die_perror("unable to allocate line settings");
+
+       gpiod_line_settings_set_direction(settings, cfg.direction);
+
+       if (cfg.bias)
+               gpiod_line_settings_set_bias(settings, cfg.bias);
+
+       if (cfg.active_low)
+               gpiod_line_settings_set_active_low(settings, true);
+
+       req_cfg = gpiod_request_config_new();
+       if (!req_cfg)
+               die_perror("unable to allocate the request config structure");
+
+       line_cfg = gpiod_line_config_new();
+       if (!line_cfg)
+               die_perror("unable to allocate the line config structure");
+
+       gpiod_request_config_set_consumer(req_cfg, cfg.consumer);
+
+       for (i = 0; i < resolver->num_chips; i++) {
+               chip = gpiod_chip_open(resolver->chips[i].path);
+               if (!chip)
+                       die_perror("unable to open chip '%s'",
+                                  resolver->chips[i].path);
+
+               num_lines = get_line_offsets_and_values(resolver, i, offsets,
+                                                       NULL);
+
+               gpiod_line_config_reset(line_cfg);
+               ret = gpiod_line_config_add_line_settings(line_cfg, offsets,
+                                                         num_lines, settings);
+               if (ret)
+                       die_perror("unable to add line settings");
+
+               request = gpiod_chip_request_lines(chip, req_cfg, line_cfg);
+               if (!request)
+                       die_perror("unable to request lines");
+
+               if (cfg.hold_period_us)
+                       usleep(cfg.hold_period_us);
+
+               ret = gpiod_line_request_get_values(request, values);
+               if (ret)
+                       die_perror("unable to read GPIO line values");
+
+               set_line_values(resolver, i, values);
+
+               gpiod_line_request_release(request);
+               gpiod_chip_close(chip);
+       }
+
+       fmt = cfg.unquoted ? "%s=%s" : "\"%s\"=%s";
+
+       for (i = 0; i < resolver->num_lines; i++) {
+               line = &resolver->lines[i];
+               if (cfg.numeric)
+                       printf("%d", line->value);
+               else
+                       printf(fmt, line->id,
+                              line->value ? "active" : "inactive");
+
+               if (i != resolver->num_lines - 1)
+                       printf(" ");
+       }
+       printf("\n");
+
+       free_line_resolver(resolver);
+       gpiod_request_config_free(req_cfg);
+       gpiod_line_config_free(line_cfg);
+       gpiod_line_settings_free(settings);
+       free(offsets);
+       free(values);
+
+       return EXIT_SUCCESS;
+}
diff --git a/tools/gpioinfo.c b/tools/gpioinfo.c
new file mode 100644 (file)
index 0000000..d5e4751
--- /dev/null
@@ -0,0 +1,271 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+// SPDX-FileCopyrightText: 2017-2021 Bartosz Golaszewski <bartekgola@gmail.com>
+// SPDX-FileCopyrightText: 2022 Kent Gibson <warthog618@gmail.com>
+
+#include <getopt.h>
+#include <gpiod.h>
+#include <stdarg.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "tools-common.h"
+
+struct config {
+       bool by_name;
+       bool strict;
+       bool unquoted_strings;
+       const char *chip_id;
+};
+
+static void print_help(void)
+{
+       printf("Usage: %s [OPTIONS] [line]...\n", get_prog_name());
+       printf("\n");
+       printf("Print information about GPIO lines.\n");
+       printf("\n");
+       printf("Lines are specified by name, or optionally by offset if the chip option\n");
+       printf("is provided.\n");
+       printf("\n");
+       printf("If no lines are specified then all lines are displayed.\n");
+       printf("\n");
+       printf("Options:\n");
+       printf("      --by-name\t\ttreat lines as names even if they would parse as an offset\n");
+       printf("  -c, --chip <chip>\trestrict scope to a particular chip\n");
+       printf("  -h, --help\t\tdisplay this help and exit\n");
+       printf("  -s, --strict\t\tcheck all lines - don't assume line names are unique\n");
+       printf("      --unquoted\tdon't quote line or consumer names\n");
+       printf("  -v, --version\t\toutput version information and exit\n");
+       print_chip_help();
+}
+
+static int parse_config(int argc, char **argv, struct config *cfg)
+{
+       static const struct option longopts[] = {
+               { "by-name",    no_argument,    NULL,           'B' },
+               { "chip",       required_argument, NULL,        'c' },
+               { "help",       no_argument,    NULL,           'h' },
+               { "strict",     no_argument,    NULL,           's' },
+               { "unquoted",   no_argument,    NULL,           'Q' },
+               { "version",    no_argument,    NULL,           'v' },
+               { GETOPT_NULL_LONGOPT },
+       };
+
+
+       static const char *const shortopts = "+c:hsv";
+
+       int opti, optc;
+
+       memset(cfg, 0, sizeof(*cfg));
+
+       for (;;) {
+               optc = getopt_long(argc, argv, shortopts, longopts, &opti);
+               if (optc < 0)
+                       break;
+
+               switch (optc) {
+               case 'B':
+                       cfg->by_name = true;
+                       break;
+               case 'c':
+                       cfg->chip_id = optarg;
+                       break;
+               case 's':
+                       cfg->strict = true;
+                       break;
+               case 'h':
+                       print_help();
+                       exit(EXIT_SUCCESS);
+               case 'Q':
+                       cfg->unquoted_strings = true;
+                       break;
+               case 'v':
+                       print_version();
+                       exit(EXIT_SUCCESS);
+               case '?':
+                       die("try %s --help", get_prog_name());
+               case 0:
+                       break;
+               default:
+                       abort();
+               }
+       }
+
+       return optind;
+}
+
+/*
+ * Minimal version similar to tools-common that indicates if a line should be
+ * printed rather than storing details into the resolver.
+ * Does not die on non-unique lines.
+ */
+static bool resolve_line(struct line_resolver *resolver,
+                        struct gpiod_line_info *info, int chip_num)
+{
+       struct resolved_line *line;
+       bool resolved = false;
+       unsigned int offset;
+       const char *name;
+       int i;
+
+       offset = gpiod_line_info_get_offset(info);
+
+       for (i = 0; i < resolver->num_lines; i++) {
+               line = &resolver->lines[i];
+
+               /* already resolved by offset? */
+               if (line->resolved && (line->offset == offset) &&
+                   (line->chip_num == chip_num)) {
+                       resolved = true;
+               }
+
+               if (line->resolved && !resolver->strict)
+                       continue;
+
+               /* else resolve by name */
+               name = gpiod_line_info_get_name(info);
+               if (name && (strcmp(line->id, name) == 0)) {
+                       line->resolved = true;
+                       line->offset = offset;
+                       line->chip_num = chip_num;
+                       resolved = true;
+               }
+       }
+
+       return resolved;
+}
+
+static void print_line_info(struct gpiod_line_info *info, bool unquoted_strings)
+{
+       char quoted_name[17];
+       const char *name;
+       int len;
+
+       name = gpiod_line_info_get_name(info);
+       if (!name) {
+               name = "unnamed";
+               unquoted_strings = true;
+       }
+
+       if (unquoted_strings) {
+               printf("%-16s\t", name);
+       } else {
+               len = strlen(name);
+               if (len <= 14) {
+                       quoted_name[0] = '"';
+                       memcpy(&quoted_name[1], name, len);
+                       quoted_name[len + 1] = '"';
+                       quoted_name[len + 2] = '\0';
+                       printf("%-16s\t", quoted_name);
+               } else {
+                       printf("\"%s\"\t", name);
+               }
+       }
+
+       print_line_attributes(info, unquoted_strings);
+}
+
+/*
+ * based on resolve_lines, but prints lines immediately rather than collecting
+ * details in the resolver.
+ */
+static void list_lines(struct line_resolver *resolver, struct gpiod_chip *chip,
+                      int chip_num, struct config *cfg)
+{
+       struct gpiod_chip_info *chip_info;
+       struct gpiod_line_info *info;
+       int offset, num_lines;
+
+       chip_info = gpiod_chip_get_info(chip);
+       if (!chip_info)
+               die_perror("unable to read info from chip %s",
+                          gpiod_chip_get_path(chip));
+
+       num_lines = gpiod_chip_info_get_num_lines(chip_info);
+
+       if ((chip_num == 0) && (cfg->chip_id && !cfg->by_name))
+               resolve_lines_by_offset(resolver, num_lines);
+
+       for (offset = 0; ((offset < num_lines) &&
+                         !(resolver->num_lines && resolve_done(resolver)));
+            offset++) {
+               info = gpiod_chip_get_line_info(chip, offset);
+               if (!info)
+                       die_perror("unable to read info for line %d from %s",
+                                  offset, gpiod_chip_info_get_name(chip_info));
+
+               if (resolver->num_lines &&
+                   !resolve_line(resolver, info, chip_num)) {
+                       gpiod_line_info_free(info);
+                       continue;
+               }
+
+               if (resolver->num_lines) {
+                       printf("%s %u", gpiod_chip_info_get_name(chip_info),
+                              offset);
+               } else {
+                       if (offset == 0)
+                               printf("%s - %u lines:\n",
+                                      gpiod_chip_info_get_name(chip_info),
+                                      num_lines);
+
+                       printf("\tline %3u:", offset);
+               }
+
+               fputc('\t', stdout);
+               print_line_info(info, cfg->unquoted_strings);
+               fputc('\n', stdout);
+               gpiod_line_info_free(info);
+               resolver->num_found++;
+       }
+
+       gpiod_chip_info_free(chip_info);
+}
+
+int main(int argc, char **argv)
+{
+       struct line_resolver *resolver = NULL;
+       int num_chips, i, ret = EXIT_SUCCESS;
+       struct gpiod_chip *chip;
+       struct config cfg;
+       char **paths;
+
+       set_prog_name(argv[0]);
+       i = parse_config(argc, argv, &cfg);
+       argc -= i;
+       argv += i;
+
+       if (!cfg.chip_id)
+               cfg.by_name = true;
+
+       num_chips = chip_paths(cfg.chip_id, &paths);
+       if (cfg.chip_id && (num_chips == 0))
+               die("cannot find GPIO chip character device '%s'", cfg.chip_id);
+
+       resolver = resolver_init(argc, argv, num_chips, cfg.strict,
+                                cfg.by_name);
+
+       for (i = 0; i < num_chips; i++) {
+               chip = gpiod_chip_open(paths[i]);
+               if (chip) {
+                       list_lines(resolver, chip, i, &cfg);
+                       gpiod_chip_close(chip);
+               } else {
+                       print_perror("unable to open chip '%s'", paths[i]);
+
+                       if (cfg.chip_id)
+                               return EXIT_FAILURE;
+
+                       ret = EXIT_FAILURE;
+               }
+               free(paths[i]);
+       }
+       free(paths);
+
+       validate_resolution(resolver, cfg.chip_id);
+       if (argc && resolver->num_found != argc)
+               ret = EXIT_FAILURE;
+       free(resolver);
+
+       return ret;
+}
diff --git a/tools/gpiomon.c b/tools/gpiomon.c
new file mode 100644 (file)
index 0000000..e3abb2d
--- /dev/null
@@ -0,0 +1,499 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+// SPDX-FileCopyrightText: 2017-2021 Bartosz Golaszewski <bartekgola@gmail.com>
+// SPDX-FileCopyrightText: 2022 Kent Gibson <warthog618@gmail.com>
+
+#include <getopt.h>
+#include <gpiod.h>
+#include <inttypes.h>
+#include <poll.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "tools-common.h"
+
+#define EVENT_BUF_SIZE 32
+
+struct config {
+       bool active_low;
+       bool banner;
+       bool by_name;
+       bool quiet;
+       bool strict;
+       bool unquoted;
+       enum gpiod_line_bias bias;
+       enum gpiod_line_edge edges;
+       int events_wanted;
+       unsigned int debounce_period_us;
+       const char *chip_id;
+       const char *consumer;
+       const char *fmt;
+       enum gpiod_line_clock event_clock;
+       int timestamp_fmt;
+       int timeout;
+};
+
+static void print_help(void)
+{
+       printf("Usage: %s [OPTIONS] <line>...\n", get_prog_name());
+       printf("\n");
+       printf("Wait for events on GPIO lines and print them to standard output.\n");
+       printf("\n");
+       printf("Lines are specified by name, or optionally by offset if the chip option\n");
+       printf("is provided.\n");
+       printf("\n");
+       printf("Options:\n");
+       printf("      --banner\t\tdisplay a banner on successful startup\n");
+       print_bias_help();
+       printf("      --by-name\t\ttreat lines as names even if they would parse as an offset\n");
+       printf("  -c, --chip <chip>\trestrict scope to a particular chip\n");
+       printf("  -C, --consumer <name>\tconsumer name applied to requested lines (default is 'gpiomon')\n");
+       printf("  -e, --edges <edges>\tspecify the edges to monitor\n");
+       printf("\t\t\tPossible values: 'falling', 'rising', 'both'.\n");
+       printf("\t\t\t(default is 'both')\n");
+       printf("  -E, --event-clock <clock>\n");
+       printf("\t\t\tspecify the source clock for event timestamps\n");
+       printf("\t\t\tPossible values: 'monotonic', 'realtime', 'hte'.\n");
+       printf("\t\t\t(default is 'monotonic')\n");
+       printf("\t\t\tBy default 'realtime' is formatted as UTC, others as raw u64.\n");
+       printf("  -h, --help\t\tdisplay this help and exit\n");
+       printf("  -F, --format <fmt>\tspecify a custom output format\n");
+       printf("      --idle-timeout <period>\n");
+       printf("\t\t\texit gracefully if no events occur for the period specified\n");
+       printf("  -l, --active-low\ttreat the line as active low, flipping the sense of\n");
+       printf("\t\t\trising and falling edges\n");
+       printf("      --localtime\tformat event timestamps as local time\n");
+       printf("  -n, --num-events <num>\n");
+       printf("\t\t\texit after processing num events\n");
+       printf("  -p, --debounce-period <period>\n");
+       printf("\t\t\tdebounce the line(s) with the specified period\n");
+       printf("  -q, --quiet\t\tdon't generate any output\n");
+       printf("  -s, --strict\t\tabort if requested line names are not unique\n");
+       printf("      --unquoted\tdon't quote line or consumer names\n");
+       printf("      --utc\t\tformat event timestamps as UTC (default for 'realtime')\n");
+       printf("  -v, --version\t\toutput version information and exit\n");
+       print_chip_help();
+       print_period_help();
+       printf("\n");
+       printf("Format specifiers:\n");
+       printf("  %%o   GPIO line offset\n");
+       printf("  %%l   GPIO line name\n");
+       printf("  %%c   GPIO chip name\n");
+       printf("  %%e   numeric edge event type ('1' - rising or '2' - falling)\n");
+       printf("  %%E   edge event type ('rising' or 'falling')\n");
+       printf("  %%S   event timestamp as seconds\n");
+       printf("  %%U   event timestamp as UTC\n");
+       printf("  %%L   event timestamp as local time\n");
+}
+
+static int parse_edges_or_die(const char *option)
+{
+       if (strcmp(option, "rising") == 0)
+               return GPIOD_LINE_EDGE_RISING;
+       if (strcmp(option, "falling") == 0)
+               return GPIOD_LINE_EDGE_FALLING;
+       if (strcmp(option, "both") != 0)
+               die("invalid edges: %s", option);
+
+       return GPIOD_LINE_EDGE_BOTH;
+}
+
+static int parse_event_clock_or_die(const char *option)
+{
+       if (strcmp(option, "realtime") == 0)
+               return GPIOD_LINE_CLOCK_REALTIME;
+       if (strcmp(option, "hte") == 0)
+               return GPIOD_LINE_CLOCK_HTE;
+       if (strcmp(option, "monotonic") != 0)
+               die("invalid event clock: %s", option);
+
+       return GPIOD_LINE_CLOCK_MONOTONIC;
+}
+
+static int parse_config(int argc, char **argv, struct config *cfg)
+{
+       static const char *const shortopts = "+b:c:C:e:E:hF:ln:p:qshv";
+
+       const struct option longopts[] = {
+               { "active-low", no_argument,    NULL,           'l' },
+               { "banner",     no_argument,    NULL,           '-'},
+               { "bias",       required_argument, NULL,        'b' },
+               { "by-name",    no_argument,    NULL,           'B'},
+               { "chip",       required_argument, NULL,        'c' },
+               { "consumer",   required_argument, NULL,        'C' },
+               { "debounce-period", required_argument, NULL,   'p' },
+               { "edges",      required_argument, NULL,        'e' },
+               { "event-clock", required_argument, NULL,       'E' },
+               { "format",     required_argument, NULL,        'F' },
+               { "help",       no_argument,    NULL,           'h' },
+               { "idle-timeout",       required_argument,      NULL,           'i' },
+               { "localtime",  no_argument,    &cfg->timestamp_fmt,    2 },
+               { "num-events", required_argument, NULL,        'n' },
+               { "quiet",      no_argument,    NULL,           'q' },
+               { "silent",     no_argument,    NULL,           'q' },
+               { "strict",     no_argument,    NULL,           's' },
+               { "unquoted",   no_argument,    NULL,           'Q' },
+               { "utc",        no_argument,    &cfg->timestamp_fmt,    1 },
+               { "version",    no_argument,    NULL,           'v' },
+               { GETOPT_NULL_LONGOPT },
+       };
+
+       int opti, optc;
+
+       memset(cfg, 0, sizeof(*cfg));
+       cfg->edges = GPIOD_LINE_EDGE_BOTH;
+       cfg->consumer = "gpiomon";
+       cfg->timeout = -1;
+
+       for (;;) {
+               optc = getopt_long(argc, argv, shortopts, longopts, &opti);
+               if (optc < 0)
+                       break;
+
+               switch (optc) {
+               case '-':
+                       cfg->banner = true;
+                       break;
+               case 'b':
+                       cfg->bias = parse_bias_or_die(optarg);
+                       break;
+               case 'B':
+                       cfg->by_name = true;
+                       break;
+               case 'c':
+                       cfg->chip_id = optarg;
+                       break;
+               case 'C':
+                       cfg->consumer = optarg;
+                       break;
+               case 'e':
+                       cfg->edges = parse_edges_or_die(optarg);
+                       break;
+               case 'E':
+                       cfg->event_clock = parse_event_clock_or_die(optarg);
+                       break;
+               case 'F':
+                       cfg->fmt = optarg;
+                       break;
+               case 'i':
+                       cfg->timeout = parse_period_or_die(optarg) / 1000;
+                       break;
+               case 'l':
+                       cfg->active_low = true;
+                       break;
+               case 'n':
+                       cfg->events_wanted = parse_uint_or_die(optarg);
+                       break;
+               case 'p':
+                       cfg->debounce_period_us = parse_period_or_die(optarg);
+                       break;
+               case 'q':
+                       cfg->quiet = true;
+                       break;
+               case 'Q':
+                       cfg->unquoted = true;
+                       break;
+               case 's':
+                       cfg->strict = true;
+                       break;
+               case 'h':
+                       print_help();
+                       exit(EXIT_SUCCESS);
+               case 'v':
+                       print_version();
+                       exit(EXIT_SUCCESS);
+               case '?':
+                       die("try %s --help", get_prog_name());
+               case 0:
+                       break;
+               default:
+                       abort();
+               }
+       }
+
+       /* setup default clock/format combinations, where not overridden */
+       if (cfg->event_clock == 0) {
+               if (cfg->timestamp_fmt)
+                       cfg->event_clock = GPIOD_LINE_CLOCK_REALTIME;
+               else
+                       cfg->event_clock = GPIOD_LINE_CLOCK_MONOTONIC;
+       } else if ((cfg->event_clock == GPIOD_LINE_CLOCK_REALTIME) &&
+                  (cfg->timestamp_fmt == 0)) {
+               cfg->timestamp_fmt = 1;
+       }
+
+       return optind;
+}
+
+static void print_banner(int num_lines, char **lines)
+{
+       int i;
+
+       if (num_lines > 1) {
+               printf("Monitoring lines ");
+
+               for (i = 0; i < num_lines - 1; i++)
+                       printf("'%s', ", lines[i]);
+
+               printf("and '%s'...\n", lines[i]);
+       } else {
+               printf("Monitoring line '%s'...\n", lines[0]);
+       }
+}
+
+static void event_print_formatted(struct gpiod_edge_event *event,
+                                 struct line_resolver *resolver, int chip_num,
+                                 struct config *cfg)
+{
+       const char *lname, *prev, *curr;
+       unsigned int offset;
+       uint64_t evtime;
+       int evtype;
+       char fmt;
+
+       offset = gpiod_edge_event_get_line_offset(event);
+       evtime = gpiod_edge_event_get_timestamp_ns(event);
+       evtype = gpiod_edge_event_get_event_type(event);
+
+       for (prev = curr = cfg->fmt;;) {
+               curr = strchr(curr, '%');
+               if (!curr) {
+                       fputs(prev, stdout);
+                       break;
+               }
+
+               if (prev != curr)
+                       fwrite(prev, curr - prev, 1, stdout);
+
+               fmt = *(curr + 1);
+
+               switch (fmt) {
+               case 'c':
+                       fputs(get_chip_name(resolver, chip_num), stdout);
+                       break;
+               case 'e':
+                       printf("%d", evtype);
+                       break;
+               case 'E':
+                       if (evtype == GPIOD_EDGE_EVENT_RISING_EDGE)
+                               fputs("rising", stdout);
+                       else
+                               fputs("falling", stdout);
+                       break;
+               case 'l':
+                       lname = get_line_name(resolver, chip_num, offset);
+                       if (!lname)
+                               lname = "unnamed";
+                       fputs(lname, stdout);
+                       break;
+               case 'L':
+                       print_event_time(evtime, 2);
+                       break;
+               case 'o':
+                       printf("%u", offset);
+                       break;
+               case 'S':
+                       print_event_time(evtime, 0);
+                       break;
+               case 'U':
+                       print_event_time(evtime, 1);
+                       break;
+               case '%':
+                       fputc('%', stdout);
+                       break;
+               case '\0':
+                       fputc('%', stdout);
+                       goto end;
+               default:
+                       fwrite(curr, 2, 1, stdout);
+                       break;
+               }
+
+               curr += 2;
+               prev = curr;
+       }
+
+end:
+       fputc('\n', stdout);
+}
+
+static void event_print_human_readable(struct gpiod_edge_event *event,
+                                      struct line_resolver *resolver,
+                                      int chip_num, struct config *cfg)
+{
+       unsigned int offset;
+       uint64_t evtime;
+
+       offset = gpiod_edge_event_get_line_offset(event);
+       evtime = gpiod_edge_event_get_timestamp_ns(event);
+
+       print_event_time(evtime, cfg->timestamp_fmt);
+
+       if (gpiod_edge_event_get_event_type(event) ==
+           GPIOD_EDGE_EVENT_RISING_EDGE)
+               fputs("\trising\t", stdout);
+       else
+               fputs("\tfalling\t", stdout);
+
+       print_line_id(resolver, chip_num, offset, cfg->chip_id, cfg->unquoted);
+       fputc('\n', stdout);
+}
+
+static void event_print(struct gpiod_edge_event *event,
+                       struct line_resolver *resolver, int chip_num,
+                       struct config *cfg)
+{
+       if (cfg->quiet)
+               return;
+
+       if (cfg->fmt)
+               event_print_formatted(event, resolver, chip_num, cfg);
+       else
+               event_print_human_readable(event, resolver, chip_num, cfg);
+}
+
+int main(int argc, char **argv)
+{
+       struct gpiod_edge_event_buffer *event_buffer;
+       struct gpiod_line_settings *settings;
+       struct gpiod_request_config *req_cfg;
+       struct gpiod_line_request **requests;
+       struct gpiod_line_config *line_cfg;
+       int num_lines, events_done = 0;
+       struct gpiod_edge_event *event;
+       struct line_resolver *resolver;
+       struct gpiod_chip *chip;
+       struct pollfd *pollfds;
+       unsigned int *offsets;
+       struct config cfg;
+       int ret, i, j;
+
+       set_prog_name(argv[0]);
+       i = parse_config(argc, argv, &cfg);
+       argc -= i;
+       argv += i;
+
+       if (argc < 1)
+               die("at least one GPIO line must be specified");
+
+       if (argc > 64)
+               die("too many lines given");
+
+       settings = gpiod_line_settings_new();
+       if (!settings)
+               die_perror("unable to allocate line settings");
+
+       if (cfg.bias)
+               gpiod_line_settings_set_bias(settings, cfg.bias);
+
+       if (cfg.active_low)
+               gpiod_line_settings_set_active_low(settings, true);
+
+       if (cfg.debounce_period_us)
+               gpiod_line_settings_set_debounce_period_us(
+                       settings, cfg.debounce_period_us);
+
+       gpiod_line_settings_set_event_clock(settings, cfg.event_clock);
+       gpiod_line_settings_set_edge_detection(settings, cfg.edges);
+
+       line_cfg = gpiod_line_config_new();
+       if (!line_cfg)
+               die_perror("unable to allocate the line config structure");
+
+       req_cfg = gpiod_request_config_new();
+       if (!req_cfg)
+               die_perror("unable to allocate the request config structure");
+
+       gpiod_request_config_set_consumer(req_cfg, cfg.consumer);
+
+       event_buffer = gpiod_edge_event_buffer_new(EVENT_BUF_SIZE);
+       if (!event_buffer)
+               die_perror("unable to allocate the line event buffer");
+
+       resolver = resolve_lines(argc, argv, cfg.chip_id, cfg.strict,
+                                cfg.by_name);
+       validate_resolution(resolver, cfg.chip_id);
+       requests = calloc(resolver->num_chips, sizeof(*requests));
+       pollfds = calloc(resolver->num_chips, sizeof(*pollfds));
+       offsets = calloc(resolver->num_lines, sizeof(*offsets));
+       if (!requests || !pollfds || !offsets)
+               die("out of memory");
+
+       for (i = 0; i < resolver->num_chips; i++) {
+               num_lines = get_line_offsets_and_values(resolver, i, offsets,
+                                                       NULL);
+               gpiod_line_config_reset(line_cfg);
+               ret = gpiod_line_config_add_line_settings(line_cfg, offsets,
+                                                         num_lines, settings);
+               if (ret)
+                       die_perror("unable to add line settings");
+
+               chip = gpiod_chip_open(resolver->chips[i].path);
+               if (!chip)
+                       die_perror("unable to open chip '%s'",
+                                  resolver->chips[i].path);
+
+               requests[i] = gpiod_chip_request_lines(chip, req_cfg, line_cfg);
+               if (!requests[i])
+                       die_perror("unable to request lines on chip %s",
+                                  resolver->chips[i].path);
+
+               pollfds[i].fd = gpiod_line_request_get_fd(requests[i]);
+               pollfds[i].events = POLLIN;
+               gpiod_chip_close(chip);
+       }
+
+       gpiod_request_config_free(req_cfg);
+       gpiod_line_config_free(line_cfg);
+       gpiod_line_settings_free(settings);
+
+       if (cfg.banner)
+               print_banner(argc, argv);
+
+       for (;;) {
+               fflush(stdout);
+
+               ret = poll(pollfds, resolver->num_chips, cfg.timeout);
+               if (ret < 0)
+                       die_perror("error polling for events");
+
+               if (ret == 0)
+                       goto done;
+
+               for (i = 0; i < resolver->num_chips; i++) {
+                       if (pollfds[i].revents == 0)
+                               continue;
+
+                       ret = gpiod_line_request_read_edge_events(requests[i],
+                                        event_buffer, EVENT_BUF_SIZE);
+                       if (ret < 0)
+                               die_perror("error reading line events");
+
+                       for (j = 0; j < ret; j++) {
+                               event = gpiod_edge_event_buffer_get_event(
+                                               event_buffer, j);
+                               if (!event)
+                                       die_perror("unable to retrieve event from buffer");
+
+                               event_print(event, resolver, i, &cfg);
+
+                               events_done++;
+
+                               if (cfg.events_wanted &&
+                                   events_done >= cfg.events_wanted)
+                                       goto done;
+                       }
+               }
+       }
+
+done:
+       for (i = 0; i < resolver->num_chips; i++)
+               gpiod_line_request_release(requests[i]);
+
+       free(requests);
+       free_line_resolver(resolver);
+       gpiod_edge_event_buffer_free(event_buffer);
+       free(offsets);
+
+       return EXIT_SUCCESS;
+}
diff --git a/tools/gpionotify.c b/tools/gpionotify.c
new file mode 100644 (file)
index 0000000..2c56590
--- /dev/null
@@ -0,0 +1,463 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+// SPDX-FileCopyrightText: 2022 Kent Gibson <warthog618@gmail.com>
+
+#include <getopt.h>
+#include <gpiod.h>
+#include <inttypes.h>
+#include <poll.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <time.h>
+
+#include "tools-common.h"
+
+struct config {
+       bool banner;
+       bool by_name;
+       bool quiet;
+       bool strict;
+       bool unquoted;
+       int event_type;
+       int events_wanted;
+       const char *chip_id;
+       const char *fmt;
+       int timestamp_fmt;
+       int timeout;
+};
+
+static void print_help(void)
+{
+       printf("Usage: %s [OPTIONS] <line>...\n", get_prog_name());
+       printf("\n");
+       printf("Wait for changes to info on GPIO lines and print them to standard output.\n");
+       printf("\n");
+       printf("Lines are specified by name, or optionally by offset if the chip option\n");
+       printf("is provided.\n");
+       printf("\n");
+       printf("Options:\n");
+       printf("      --banner\t\tdisplay a banner on successful startup\n");
+       printf("      --by-name\t\ttreat lines as names even if they would parse as an offset\n");
+       printf("  -c, --chip <chip>\trestrict scope to a particular chip\n");
+       printf("  -e, --event <event>\tspecify the events to monitor\n");
+       printf("\t\t\tPossible values: 'requested', 'released', 'reconfigured'.\n");
+       printf("\t\t\t(default is all events)\n");
+       printf("  -h, --help\t\tdisplay this help and exit\n");
+       printf("  -F, --format <fmt>\tspecify a custom output format\n");
+       printf("      --idle-timeout <period>\n");
+       printf("\t\t\texit gracefully if no events occur for the period specified\n");
+       printf("      --localtime\tconvert event timestamps to local time\n");
+       printf("  -n, --num-events <num>\n");
+       printf("\t\t\texit after processing num events\n");
+       printf("  -q, --quiet\t\tdon't generate any output\n");
+       printf("  -s, --strict\t\tabort if requested line names are not unique\n");
+       printf("      --unquoted\tdon't quote line or consumer names\n");
+       printf("      --utc\t\tconvert event timestamps to UTC\n");
+       printf("  -v, --version\t\toutput version information and exit\n");
+       print_chip_help();
+       print_period_help();
+       printf("\n");
+       printf("Format specifiers:\n");
+       printf("  %%o   GPIO line offset\n");
+       printf("  %%l   GPIO line name\n");
+       printf("  %%c   GPIO chip name\n");
+       printf("  %%e   numeric info event type ('1' - requested, '2' - released or '3' - reconfigured)\n");
+       printf("  %%E   info event type ('requested', 'released' or 'reconfigured')\n");
+       printf("  %%a   line attributes\n");
+       printf("  %%C   consumer\n");
+       printf("  %%S   event timestamp as seconds\n");
+       printf("  %%U   event timestamp as UTC\n");
+       printf("  %%L   event timestamp as local time\n");
+}
+
+static int parse_event_type_or_die(const char *option)
+{
+       if (strcmp(option, "requested") == 0)
+               return GPIOD_INFO_EVENT_LINE_REQUESTED;
+       if (strcmp(option, "released") == 0)
+               return GPIOD_INFO_EVENT_LINE_RELEASED;
+       if (strcmp(option, "reconfigured") != 0)
+               die("invalid edge: %s", option);
+
+       return GPIOD_INFO_EVENT_LINE_CONFIG_CHANGED;
+}
+
+static int parse_config(int argc, char **argv, struct config *cfg)
+{
+       static const char *const shortopts = "+c:e:hF:n:qshv";
+
+       const struct option longopts[] = {
+               { "banner",     no_argument,    NULL,           '-'},
+               { "by-name",    no_argument,    NULL,           'B'},
+               { "chip",       required_argument, NULL,        'c' },
+               { "event",      required_argument, NULL,        'e' },
+               { "format",     required_argument, NULL,        'F' },
+               { "help",       no_argument,    NULL,           'h' },
+               { "idle-timeout",       required_argument,      NULL,           'i' },
+               { "localtime",  no_argument,    &cfg->timestamp_fmt, 2 },
+               { "num-events", required_argument, NULL,        'n' },
+               { "quiet",      no_argument,    NULL,           'q' },
+               { "silent",     no_argument,    NULL,           'q' },
+               { "strict",     no_argument,    NULL,           's' },
+               { "unquoted",   no_argument,    NULL,           'Q' },
+               { "utc",        no_argument,    &cfg->timestamp_fmt, 1 },
+               { "version",    no_argument,    NULL,           'v' },
+               { GETOPT_NULL_LONGOPT },
+       };
+
+       int opti, optc;
+
+       memset(cfg, 0, sizeof(*cfg));
+       cfg->timeout = -1;
+
+       for (;;) {
+               optc = getopt_long(argc, argv, shortopts, longopts, &opti);
+               if (optc < 0)
+                       break;
+
+               switch (optc) {
+               case '-':
+                       cfg->banner = true;
+                       break;
+               case 'B':
+                       cfg->by_name = true;
+                       break;
+               case 'c':
+                       cfg->chip_id = optarg;
+                       break;
+               case 'e':
+                       cfg->event_type = parse_event_type_or_die(optarg);
+                       break;
+               case 'F':
+                       cfg->fmt = optarg;
+                       break;
+               case 'i':
+                       cfg->timeout = parse_period_or_die(optarg) / 1000;
+                       break;
+               case 'n':
+                       cfg->events_wanted = parse_uint_or_die(optarg);
+                       break;
+               case 'q':
+                       cfg->quiet = true;
+                       break;
+               case 'Q':
+                       cfg->unquoted = true;
+                       break;
+               case 's':
+                       cfg->strict = true;
+                       break;
+               case 'h':
+                       print_help();
+                       exit(EXIT_SUCCESS);
+               case 'v':
+                       print_version();
+                       exit(EXIT_SUCCESS);
+               case '?':
+                       die("try %s --help", get_prog_name());
+               case 0:
+                       break;
+               default:
+                       abort();
+               }
+       }
+
+       return optind;
+}
+
+static void print_banner(int num_lines, char **lines)
+{
+       int i;
+
+       if (num_lines > 1) {
+               printf("Watching lines ");
+
+               for (i = 0; i < num_lines - 1; i++)
+                       printf("'%s', ", lines[i]);
+
+               printf("and '%s'...\n", lines[i]);
+       } else {
+               printf("Watching line '%s'...\n", lines[0]);
+       }
+}
+
+static void print_event_type(int evtype)
+{
+       switch (evtype) {
+       case GPIOD_INFO_EVENT_LINE_REQUESTED:
+               fputs("requested", stdout);
+               break;
+       case GPIOD_INFO_EVENT_LINE_RELEASED:
+               fputs("released", stdout);
+               break;
+       case GPIOD_INFO_EVENT_LINE_CONFIG_CHANGED:
+               fputs("reconfigured", stdout);
+               break;
+       default:
+               fputs("unknown", stdout);
+               break;
+       }
+}
+
+/*
+ * A convenience function to map clock monotonic to realtime, as uAPI only
+ * supports CLOCK_MONOTONIC.
+ *
+ * Samples the realtime clock on either side of a monotonic sample and averages
+ * the realtime samples to estimate the offset between the two clocks.
+ * Any time shifts between the two realtime samples will result in the
+ * monotonic time being mapped to the average of the before and after, so
+ * half way between the old and new times.
+ *
+ * Any CPU suspension between the event being generated and converted will
+ * result in the returned time being shifted by the period of suspension.
+ */
+static uint64_t monotonic_to_realtime(uint64_t evtime)
+{
+       uint64_t before, after, mono;
+       struct timespec ts;
+
+       clock_gettime(CLOCK_REALTIME, &ts);
+       before = ts.tv_nsec + ((uint64_t)ts.tv_sec) * 1000000000;
+
+       clock_gettime(CLOCK_MONOTONIC, &ts);
+       mono = ts.tv_nsec + ((uint64_t)ts.tv_sec) * 1000000000;
+
+       clock_gettime(CLOCK_REALTIME, &ts);
+       after = ts.tv_nsec + ((uint64_t)ts.tv_sec) * 1000000000;
+
+       evtime += (after / 2 - mono + before / 2);
+
+       return evtime;
+}
+
+static void event_print_formatted(struct gpiod_info_event *event,
+                                 struct line_resolver *resolver, int chip_num,
+                                 struct config *cfg)
+{
+       const char *lname, *prev, *curr, *consumer;
+       struct gpiod_line_info *info;
+       unsigned int offset;
+       uint64_t evtime;
+       int evtype;
+       char fmt;
+
+       info = gpiod_info_event_get_line_info(event);
+       evtime = gpiod_info_event_get_timestamp_ns(event);
+       evtype = gpiod_info_event_get_event_type(event);
+       offset = gpiod_line_info_get_offset(info);
+
+       for (prev = curr = cfg->fmt;;) {
+               curr = strchr(curr, '%');
+               if (!curr) {
+                       fputs(prev, stdout);
+                       break;
+               }
+
+               if (prev != curr)
+                       fwrite(prev, curr - prev, 1, stdout);
+
+               fmt = *(curr + 1);
+
+               switch (fmt) {
+               case 'a':
+                       print_line_attributes(info, cfg->unquoted);
+                       break;
+               case 'c':
+                       fputs(get_chip_name(resolver, chip_num), stdout);
+                       break;
+               case 'C':
+                       if (!gpiod_line_info_is_used(info)) {
+                               consumer = "unused";
+                       } else {
+                               consumer = gpiod_line_info_get_consumer(info);
+                               if (!consumer)
+                                       consumer = "kernel";
+                       }
+                       fputs(consumer, stdout);
+                       break;
+               case 'e':
+                       printf("%d", evtype);
+                       break;
+               case 'E':
+                       print_event_type(evtype);
+                       break;
+               case 'l':
+                       lname = gpiod_line_info_get_name(info);
+                       if (!lname)
+                               lname = "unnamed";
+                       fputs(lname, stdout);
+                       break;
+               case 'L':
+                       print_event_time(monotonic_to_realtime(evtime), 2);
+                       break;
+               case 'o':
+                       printf("%u", offset);
+                       break;
+               case 'S':
+                       print_event_time(evtime, 0);
+                       break;
+               case 'U':
+                       print_event_time(monotonic_to_realtime(evtime), 1);
+                       break;
+               case '%':
+                       fputc('%', stdout);
+                       break;
+               case '\0':
+                       fputc('%', stdout);
+                       goto end;
+               default:
+                       fwrite(curr, 2, 1, stdout);
+                       break;
+               }
+
+               curr += 2;
+               prev = curr;
+       }
+
+end:
+       fputc('\n', stdout);
+}
+
+static void event_print_human_readable(struct gpiod_info_event *event,
+                                      struct line_resolver *resolver,
+                                      int chip_num, struct config *cfg)
+{
+       struct gpiod_line_info *info;
+       unsigned int offset;
+       uint64_t evtime;
+       char *evname;
+       int evtype;
+
+       info = gpiod_info_event_get_line_info(event);
+       evtime = gpiod_info_event_get_timestamp_ns(event);
+       evtype = gpiod_info_event_get_event_type(event);
+       offset = gpiod_line_info_get_offset(info);
+
+       switch (evtype) {
+       case GPIOD_INFO_EVENT_LINE_REQUESTED:
+               evname = "requested";
+               break;
+       case GPIOD_INFO_EVENT_LINE_RELEASED:
+               evname = "released";
+               break;
+       case GPIOD_INFO_EVENT_LINE_CONFIG_CHANGED:
+               evname = "reconfigured";
+               break;
+       default:
+               evname = "unknown";
+       }
+
+       if (cfg->timestamp_fmt)
+               evtime = monotonic_to_realtime(evtime);
+
+       print_event_time(evtime, cfg->timestamp_fmt);
+       printf("\t%s\t", evname);
+       print_line_id(resolver, chip_num, offset, cfg->chip_id, cfg->unquoted);
+       fputc('\n', stdout);
+}
+
+static void event_print(struct gpiod_info_event *event,
+                       struct line_resolver *resolver, int chip_num,
+                       struct config *cfg)
+{
+       if (cfg->quiet)
+               return;
+
+       if (cfg->fmt)
+               event_print_formatted(event, resolver, chip_num, cfg);
+       else
+               event_print_human_readable(event, resolver, chip_num, cfg);
+}
+
+int main(int argc, char **argv)
+{
+       int i, j, ret, events_done = 0, evtype;
+       struct line_resolver *resolver;
+       struct gpiod_info_event *event;
+       struct gpiod_chip **chips;
+       struct gpiod_chip *chip;
+       struct pollfd *pollfds;
+       struct config cfg;
+
+       set_prog_name(argv[0]);
+       i = parse_config(argc, argv, &cfg);
+       argc -= optind;
+       argv += optind;
+
+       if (argc < 1)
+               die("at least one GPIO line must be specified");
+
+       if (argc > 64)
+               die("too many lines given");
+
+       resolver = resolve_lines(argc, argv, cfg.chip_id, cfg.strict,
+                                cfg.by_name);
+       validate_resolution(resolver, cfg.chip_id);
+       chips = calloc(resolver->num_chips, sizeof(*chips));
+       pollfds = calloc(resolver->num_chips, sizeof(*pollfds));
+       if (!pollfds)
+               die("out of memory");
+
+       for (i = 0; i < resolver->num_chips; i++) {
+               chip = gpiod_chip_open(resolver->chips[i].path);
+               if (!chip)
+                       die_perror("unable to open chip '%s'",
+                                  resolver->chips[i].path);
+
+               for (j = 0; j < resolver->num_lines; j++)
+                       if ((resolver->lines[j].chip_num == i) &&
+                           !gpiod_chip_watch_line_info(
+                                   chip, resolver->lines[j].offset))
+                               die_perror("unable to watch line on chip '%s'",
+                                          resolver->chips[i].path);
+
+               chips[i] = chip;
+               pollfds[i].fd = gpiod_chip_get_fd(chip);
+               pollfds[i].events = POLLIN;
+       }
+
+       if (cfg.banner)
+               print_banner(argc, argv);
+
+       for (;;) {
+               fflush(stdout);
+
+               ret = poll(pollfds, resolver->num_chips, cfg.timeout);
+               if (ret < 0)
+                       die_perror("error polling for events");
+
+               if (ret == 0)
+                       goto done;
+
+               for (i = 0; i < resolver->num_chips; i++) {
+                       if (pollfds[i].revents == 0)
+                               continue;
+
+                       event = gpiod_chip_read_info_event(chips[i]);
+                       if (!event)
+                               die_perror("unable to retrieve chip event");
+
+                       if (cfg.event_type) {
+                               evtype = gpiod_info_event_get_event_type(event);
+                               if (evtype != cfg.event_type)
+                                       continue;
+                       }
+
+                       event_print(event, resolver, i, &cfg);
+
+                       events_done++;
+
+                       if (cfg.events_wanted &&
+                           events_done >= cfg.events_wanted)
+                               goto done;
+               }
+       }
+done:
+       for (i = 0; i < resolver->num_chips; i++)
+               gpiod_chip_close(chips[i]);
+
+       free(chips);
+       free_line_resolver(resolver);
+
+       return EXIT_SUCCESS;
+}
diff --git a/tools/gpioset.c b/tools/gpioset.c
new file mode 100644 (file)
index 0000000..9dc5aeb
--- /dev/null
@@ -0,0 +1,1008 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+// SPDX-FileCopyrightText: 2017-2021 Bartosz Golaszewski <bartekgola@gmail.com>
+// SPDX-FileCopyrightText: 2022 Kent Gibson <warthog618@gmail.com>
+
+#include <ctype.h>
+#include <gpiod.h>
+#include <getopt.h>
+#include <limits.h>
+#include <poll.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+#ifdef GPIOSET_INTERACTIVE
+#include <editline/readline.h>
+#endif
+
+#include "tools-common.h"
+
+struct config {
+       bool active_low;
+       bool banner;
+       bool by_name;
+       bool daemonize;
+       bool interactive;
+       bool strict;
+       bool unquoted;
+       enum gpiod_line_bias bias;
+       enum gpiod_line_drive drive;
+       int toggles;
+       unsigned int *toggle_periods;
+       unsigned int hold_period_us;
+       const char *chip_id;
+       const char *consumer;
+};
+
+static void print_help(void)
+{
+       printf("Usage: %s [OPTIONS] <line=value>...\n", get_prog_name());
+       printf("\n");
+       printf("Set values of GPIO lines.\n");
+       printf("\n");
+       printf("Lines are specified by name, or optionally by offset if the chip option\n");
+       printf("is provided.\n");
+       printf("Values may be '1' or '0', or equivalently 'active'/'inactive' or 'on'/'off'.\n");
+       printf("\n");
+       printf("The line output state is maintained until the process exits, but after that\n");
+       printf("is not guaranteed.\n");
+       printf("\n");
+       printf("Options:\n");
+       printf("      --banner\t\tdisplay a banner on successful startup\n");
+       print_bias_help();
+       printf("      --by-name\t\ttreat lines as names even if they would parse as an offset\n");
+       printf("  -c, --chip <chip>\trestrict scope to a particular chip\n");
+       printf("  -C, --consumer <name>\tconsumer name applied to requested lines (default is 'gpioset')\n");
+       printf("  -d, --drive <drive>\tspecify the line drive mode\n");
+       printf("\t\t\tPossible values: 'push-pull', 'open-drain', 'open-source'.\n");
+       printf("\t\t\t(default is 'push-pull')\n");
+       printf("  -h, --help\t\tdisplay this help and exit\n");
+#ifdef GPIOSET_INTERACTIVE
+       printf("  -i, --interactive\tset the lines then wait for additional set commands\n");
+       printf("\t\t\tUse the 'help' command at the interactive prompt to get help\n");
+       printf("\t\t\tfor the supported commands.\n");
+#endif
+       printf("  -l, --active-low\ttreat the line as active low\n");
+       printf("  -p, --hold-period <period>\n");
+       printf("\t\t\tthe minimum time period to hold lines at the requested values\n");
+       printf("  -s, --strict\t\tabort if requested line names are not unique\n");
+       printf("  -t, --toggle <period>[,period]...\n");
+       printf("\t\t\ttoggle the line(s) after the specified period(s)\n");
+       printf("\t\t\tIf the last period is non-zero then the sequence repeats.\n");
+       printf("      --unquoted\tdon't quote line names\n");
+       printf("  -v, --version\t\toutput version information and exit\n");
+       printf("  -z, --daemonize\tset values then detach from the controlling terminal\n");
+       print_chip_help();
+       print_period_help();
+       printf("\n");
+       printf("*Note*\n");
+       printf("    The state of a GPIO line controlled over the character device reverts to default\n");
+       printf("    when the last process referencing the file descriptor representing the device file exits.\n");
+       printf("    This means that it's wrong to run gpioset, have it exit and expect the line to continue\n");
+       printf("    being driven high or low. It may happen if given pin is floating but it must be interpreted\n");
+       printf("    as undefined behavior.\n");
+}
+
+static int parse_drive_or_die(const char *option)
+{
+       if (strcmp(option, "open-drain") == 0)
+               return GPIOD_LINE_DRIVE_OPEN_DRAIN;
+       if (strcmp(option, "open-source") == 0)
+               return GPIOD_LINE_DRIVE_OPEN_SOURCE;
+       if (strcmp(option, "push-pull") != 0)
+               die("invalid drive: %s", option);
+
+       return 0;
+}
+
+static int parse_periods_or_die(char *option, unsigned int **periods)
+{
+       int i, num_periods = 1;
+       unsigned int *pp;
+       char *end;
+
+       for (i = 0; option[i] != '\0'; i++)
+               if (option[i] == ',')
+                       num_periods++;
+
+       pp = calloc(num_periods, sizeof(*pp));
+       if (pp == NULL)
+               die("out of memory");
+
+       for (i = 0; i < num_periods - 1; i++) {
+               for (end = option; *end != ','; end++)
+                       ;
+
+               *end = '\0';
+               pp[i] = parse_period_or_die(option);
+               option = end + 1;
+       }
+       pp[i] = parse_period_or_die(option);
+       *periods = pp;
+
+       return num_periods;
+}
+
+static int parse_config(int argc, char **argv, struct config *cfg)
+{
+       static const struct option longopts[] = {
+               { "active-low", no_argument,            NULL,   'l' },
+               { "banner",     no_argument,            NULL,   '-'},
+               { "bias",       required_argument,      NULL,   'b' },
+               { "by-name",    no_argument,            NULL,   'B' },
+               { "chip",       required_argument,      NULL,   'c' },
+               { "consumer",   required_argument,      NULL,   'C' },
+               { "daemonize",  no_argument,            NULL,   'z' },
+               { "drive",      required_argument,      NULL,   'd' },
+               { "help",       no_argument,            NULL,   'h' },
+               { "hold-period", required_argument,     NULL,   'p' },
+#ifdef GPIOSET_INTERACTIVE
+               { "interactive", no_argument,           NULL,   'i' },
+#endif
+               { "strict",     no_argument,            NULL,   's' },
+               { "toggle",     required_argument,      NULL,   't' },
+               { "unquoted",   no_argument,            NULL,   'Q' },
+               { "version",    no_argument,            NULL,   'v' },
+               { GETOPT_NULL_LONGOPT },
+       };
+
+#ifdef GPIOSET_INTERACTIVE
+       static const char *const shortopts = "+b:c:C:d:hilp:st:vz";
+#else
+       static const char *const shortopts = "+b:c:C:d:hlp:st:vz";
+#endif
+
+       int opti, optc;
+
+       memset(cfg, 0, sizeof(*cfg));
+       cfg->consumer = "gpioset";
+
+       for (;;) {
+               optc = getopt_long(argc, argv, shortopts, longopts, &opti);
+               if (optc < 0)
+                       break;
+
+               switch (optc) {
+               case '-':
+                       cfg->banner = true;
+                       break;
+               case 'b':
+                       cfg->bias = parse_bias_or_die(optarg);
+                       break;
+               case 'B':
+                       cfg->by_name = true;
+                       break;
+               case 'c':
+                       cfg->chip_id = optarg;
+                       break;
+               case 'C':
+                       cfg->consumer = optarg;
+                       break;
+               case 'd':
+                       cfg->drive = parse_drive_or_die(optarg);
+                       break;
+#ifdef GPIOSET_INTERACTIVE
+               case 'i':
+                       cfg->interactive = true;
+                       break;
+#endif
+               case 'l':
+                       cfg->active_low = true;
+                       break;
+               case 'p':
+                       cfg->hold_period_us = parse_period_or_die(optarg);
+                       break;
+               case 'Q':
+                       cfg->unquoted = true;
+                       break;
+               case 's':
+                       cfg->strict = true;
+                       break;
+               case 't':
+                       cfg->toggles = parse_periods_or_die(optarg,
+                                                &cfg->toggle_periods);
+                       break;
+               case 'z':
+                       cfg->daemonize = true;
+                       break;
+               case 'h':
+                       print_help();
+                       exit(EXIT_SUCCESS);
+               case 'v':
+                       print_version();
+                       exit(EXIT_SUCCESS);
+               case '?':
+                       die("try %s --help", get_prog_name());
+               case 0:
+                       break;
+               default:
+                       abort();
+               }
+       }
+
+#ifdef GPIOSET_INTERACTIVE
+       if (cfg->toggles && cfg->interactive)
+               die("can't combine interactive with toggle");
+#endif
+
+       return optind;
+}
+
+static enum gpiod_line_value parse_value(const char *option)
+{
+       if (strcmp(option, "0") == 0)
+               return GPIOD_LINE_VALUE_INACTIVE;
+       if (strcmp(option, "1") == 0)
+               return GPIOD_LINE_VALUE_ACTIVE;
+       if (strcmp(option, "inactive") == 0)
+               return GPIOD_LINE_VALUE_INACTIVE;
+       if (strcmp(option, "active") == 0)
+               return GPIOD_LINE_VALUE_ACTIVE;
+       if (strcmp(option, "off") == 0)
+               return GPIOD_LINE_VALUE_INACTIVE;
+       if (strcmp(option, "on") == 0)
+               return GPIOD_LINE_VALUE_ACTIVE;
+       if (strcmp(option, "false") == 0)
+               return GPIOD_LINE_VALUE_INACTIVE;
+       if (strcmp(option, "true") == 0)
+               return GPIOD_LINE_VALUE_ACTIVE;
+
+       return GPIOD_LINE_VALUE_ERROR;
+}
+
+/*
+ * Parse line id and values from lvs into lines and values.
+ *
+ * Accepted forms:
+ *     'line=value'
+ *     '"line"=value'
+ *
+ * If line id is quoted then it is returned unquoted.
+ */
+static bool parse_line_values(int num_lines, char **lvs, char **lines,
+                             enum gpiod_line_value *values, bool interactive)
+{
+       char *value, *line;
+       int i;
+
+       for (i = 0; i < num_lines; i++) {
+               line = lvs[i];
+
+               if (*line != '"') {
+                       value = strchr(line, '=');
+               } else {
+                       line++;
+                       value = strstr(line, "\"=");
+                       if (value) {
+                               *value = '\0';
+                               value++;
+                       }
+               }
+
+               if (!value) {
+                       if (interactive)
+                               printf("invalid line value: '%s'\n", lvs[i]);
+                       else
+                               print_error("invalid line value: '%s'", lvs[i]);
+
+                       return false;
+               }
+
+               *value = '\0';
+               value++;
+               values[i] = parse_value(value);
+
+               if (values[i] == GPIOD_LINE_VALUE_ERROR) {
+                       if (interactive)
+                               printf("invalid line value: '%s'\n", value);
+                       else
+                               print_error("invalid line value: '%s'", value);
+
+                       return false;
+               }
+
+               lines[i] = line;
+       }
+
+       return true;
+}
+
+/*
+ * Parse line id and values from lvs into lines and values, or die trying.
+ */
+static void parse_line_values_or_die(int num_lines, char **lvs, char **lines,
+                                    enum gpiod_line_value *values)
+{
+       if (!parse_line_values(num_lines, lvs, lines, values, false))
+               exit(EXIT_FAILURE);
+}
+
+static void print_banner(int num_lines, char **lines)
+{
+       int i;
+
+       if (num_lines > 1) {
+               printf("Setting lines ");
+
+               for (i = 0; i < num_lines - 1; i++)
+                       printf("'%s', ", lines[i]);
+
+               printf("and '%s'...\n", lines[i]);
+       } else {
+               printf("Setting line '%s'...\n", lines[0]);
+       }
+       fflush(stdout);
+}
+
+static void wait_fd(int fd)
+{
+       struct pollfd pfd;
+
+       pfd.fd = fd;
+       pfd.events = POLLERR;
+
+       if (poll(&pfd, 1, -1) < 0)
+               die_perror("error waiting on request");
+}
+
+/*
+ * Apply values from the resolver to the requests.
+ * offset and values are scratch pads for working.
+ */
+static void apply_values(struct gpiod_line_request **requests,
+                        struct line_resolver *resolver, unsigned int *offsets,
+                        enum gpiod_line_value *values)
+{
+       int i;
+
+       for (i = 0; i < resolver->num_chips; i++) {
+               get_line_offsets_and_values(resolver, i, offsets, values);
+               if (gpiod_line_request_set_values(requests[i], values))
+                       print_perror("unable to set values on '%s'",
+                                    get_chip_name(resolver, i));
+       }
+}
+
+/* Toggle the values of all lines in the resolver */
+static void toggle_all_lines(struct line_resolver *resolver)
+{
+       int i;
+
+       for (i = 0; i < resolver->num_lines; i++)
+               resolver->lines[i].value = !resolver->lines[i].value;
+}
+
+/*
+ * Toggle the resolved lines as specified by the toggle_periods,
+ * and apply the values to the requests.
+ * offset and values are scratch pads for working.
+ */
+static void toggle_sequence(int toggles, unsigned int *toggle_periods,
+                           struct gpiod_line_request **requests,
+                           struct line_resolver *resolver,
+                           unsigned int *offsets,
+                           enum gpiod_line_value *values)
+{
+       int i = 0;
+
+       if ((toggles == 1) && (toggle_periods[0] == 0))
+               return;
+
+       for (;;) {
+               usleep(toggle_periods[i]);
+               toggle_all_lines(resolver);
+               apply_values(requests, resolver, offsets, values);
+
+               i++;
+               if ((i == toggles - 1) && (toggle_periods[i] == 0))
+                       return;
+
+               if (i == toggles)
+                       i = 0;
+       }
+}
+
+#ifdef GPIOSET_INTERACTIVE
+
+/*
+ * Parse line id from words into lines.
+ *
+ * If line id is quoted then it is returned unquoted.
+ */
+static bool parse_line_ids(int num_lines, char **words, char **lines)
+{
+       int i, len;
+       char *line;
+
+       for (i = 0; i < num_lines; i++) {
+               line = words[i];
+               if (*line == '"') {
+                       line++;
+                       len = strlen(line);
+                       if ((len == 0) || line[len - 1] != '"') {
+                               printf("invalid line id: '%s'\n", words[i]);
+                               return false;
+                       }
+                       line[len - 1] = '\0';
+               }
+               lines[i] = line;
+       }
+
+       return true;
+}
+
+/*
+ * Set the values in the resolver for the line values specified by
+ * the remaining parameters.
+ */
+static void set_line_values_subset(struct line_resolver *resolver,
+                                  int num_lines, char **lines,
+                                  enum gpiod_line_value *values)
+{
+       int l, i;
+
+       for (l = 0; l < num_lines; l++) {
+               for (i = 0; i < resolver->num_lines; i++) {
+                       if (strcmp(lines[l], resolver->lines[i].id) == 0) {
+                               resolver->lines[i].value = values[l];
+                               break;
+                       }
+               }
+       }
+}
+
+static void print_all_line_values(struct line_resolver *resolver, bool unquoted)
+{
+       char *fmt = unquoted ? "%s=%s " : "\"%s\"=%s ";
+       int i;
+
+       for (i = 0; i < resolver->num_lines; i++) {
+               if (i == resolver->num_lines - 1)
+                       fmt = unquoted ? "%s=%s\n" : "\"%s\"=%s\n";
+
+               printf(fmt, resolver->lines[i].id,
+                      resolver->lines[i].value ? "active" : "inactive");
+       }
+}
+
+/*
+ * Print the resovler line values for a subset of lines, specified by
+ * num_lines and lines.
+ */
+static void print_line_values(struct line_resolver *resolver, int num_lines,
+                             char **lines, bool unquoted)
+{
+       char *fmt = unquoted ? "%s=%s " : "\"%s\"=%s ";
+       struct resolved_line *line;
+       int i, j;
+
+       for (i = 0; i < num_lines; i++) {
+               if (i == num_lines - 1)
+                       fmt = unquoted ? "%s=%s\n" : "\"%s\"=%s\n";
+
+               for (j = 0; j < resolver->num_lines; j++) {
+                       line = &resolver->lines[j];
+                       if (strcmp(lines[i], line->id) == 0) {
+                               printf(fmt, line->id,
+                                      line->value ? "active" : "inactive");
+                               break;
+                       }
+               }
+       }
+}
+
+/*
+ * Toggle a subset of lines, specified by num_lines and lines, in the resolver.
+ */
+static void toggle_lines(struct line_resolver *resolver, int num_lines,
+                        char **lines)
+{
+       struct resolved_line *line;
+       int i, j;
+
+       for (i = 0; i < num_lines; i++)
+               for (j = 0; j < resolver->num_lines; j++) {
+                       line = &resolver->lines[j];
+                       if (strcmp(lines[i], line->id) == 0) {
+                               line->value = !line->value;
+                               break;
+                       }
+               }
+}
+
+/*
+ * Check that a set of lines, specified by num_lines and lines, are all
+ * resolved lines.
+ */
+static bool valid_lines(struct line_resolver *resolver, int num_lines,
+                       char **lines)
+{
+       bool ret = true, found;
+       int i, l;
+
+       for (l = 0; l < num_lines; l++) {
+               found = false;
+
+               for (i = 0; i < resolver->num_lines; i++) {
+                       if (strcmp(lines[l], resolver->lines[i].id) == 0) {
+                               found = true;
+                               break;
+                       }
+               }
+
+               if (!found) {
+                       printf("unknown line: '%s'\n", lines[l]);
+                       ret = false;
+               }
+       }
+
+       return ret;
+}
+
+static void print_interactive_help(void)
+{
+       printf("COMMANDS:\n\n");
+       printf("    exit\n");
+       printf("        Exit the program\n");
+       printf("    get [line]...\n");
+       printf("        Display the output values of the given requested lines\n\n");
+       printf("        If no lines are specified then all requested lines are displayed\n\n");
+       printf("    help\n");
+       printf("        Print this help\n\n");
+       printf("    set <line=value>...\n");
+       printf("        Update the output values of the given requested lines\n\n");
+       printf("    sleep <period>\n");
+       printf("        Sleep for the specified period\n\n");
+       printf("    toggle [line]...\n");
+       printf("        Toggle the output values of the given requested lines\n\n");
+       printf("        If no lines are specified then all requested lines are toggled\n\n");
+}
+
+/*
+ * Split a line into words, returning the each of the words and the count.
+ *
+ * max_words specifies the max number of words that may be returned in words.
+ *
+ * Any escaping is ignored, on the assumption that the only escaped
+ * character of consequence is '"', and that names won't include quotes.
+ */
+static int split_words(char *line, int max_words, char **words)
+{
+       bool in_quotes = false, in_word = false;
+       int num_words = 0;
+
+       for (; (*line != '\0'); line++) {
+               if (!in_word) {
+                       if (isspace(*line))
+                               continue;
+
+                       in_word = true;
+                       in_quotes = (*line == '"');
+
+                       /* count all words, but only store max_words */
+                       if (num_words < max_words)
+                               words[num_words] = line;
+               } else {
+                       if (in_quotes) {
+                               if (*line == '"')
+                                       in_quotes = false;
+                               continue;
+                       }
+                       if (isspace(*line)) {
+                               num_words++;
+                               in_word = false;
+                               *line = '\0';
+                       }
+               }
+       }
+
+       if (in_word)
+               num_words++;
+
+       return num_words;
+}
+
+/* check if a line is specified somewhere in the rl_line_buffer */
+static bool in_line_buffer(const char *id)
+{
+       char *match = rl_line_buffer;
+       int len = strlen(id);
+
+       while ((match = strstr(match, id))) {
+               if ((match > rl_line_buffer && isspace(match[-1])) &&
+                   ((match[len] == '=') || isspace(match[len])))
+                       return true;
+
+               match += len;
+       }
+
+       return false;
+}
+
+/* context for complete_line_id, so it can provide valid line ids */
+static struct line_resolver *completion_context;
+
+/* tab completion helper for line ids */
+static char *complete_line_id(const char *text, int state)
+{
+       static int idx, len;
+       const char *id;
+
+       if (!state) {
+               idx = 0;
+               len = strlen(text);
+       }
+
+       while (idx < completion_context->num_lines) {
+               id = completion_context->lines[idx].id;
+               idx++;
+
+               if ((strncmp(id, text, len) == 0) && (!in_line_buffer(id)))
+                       return strdup(id);
+       }
+
+       return NULL;
+}
+
+/* tab completion helper for line values (just the value component) */
+static char *complete_value(const char *text, int state)
+{
+       static const char * const values[] = {
+               "1", "0", "active", "inactive", "on", "off", "true", "false",
+               NULL
+       };
+
+       static int idx, len;
+
+       const char *value;
+
+       if (!state) {
+               idx = 0;
+               len = strlen(text);
+       }
+
+       while ((value = values[idx])) {
+               idx++;
+               if (strncmp(value, text, len) == 0)
+                       return strdup(value);
+       }
+
+       return NULL;
+}
+
+/* tab completion help for interactive commands */
+static char *complete_command(const char *text, int state)
+{
+       static const char * const commands[] = {
+               "get", "set", "toggle", "sleep", "help", "exit", NULL
+       };
+
+       static int idx, len;
+
+       const char *cmd;
+
+       if (!state) {
+               idx = 0;
+               len = strlen(text);
+       }
+
+       while ((cmd = commands[idx])) {
+               idx++;
+               if (strncmp(cmd, text, len) == 0)
+                       return strdup(cmd);
+       }
+       return NULL;
+}
+
+/* tab completion for interactive command lines */
+static char **tab_completion(const char *text, int start, int end)
+{
+       int cmd_start, cmd_end, len;
+       char **matches = NULL;
+
+       rl_attempted_completion_over = true;
+       rl_completion_type = '@';
+       rl_completion_append_character = ' ';
+
+       for (cmd_start = 0;
+            isspace(rl_line_buffer[cmd_start]) && cmd_start < end; cmd_start++)
+               ;
+
+       if (cmd_start == start)
+               matches = rl_completion_matches(text, complete_command);
+
+       for (cmd_end = cmd_start + 1;
+            !isspace(rl_line_buffer[cmd_end]) && cmd_end < end; cmd_end++)
+               ;
+
+       len = cmd_end - cmd_start;
+       if (len == 3 && strncmp("set ", &rl_line_buffer[cmd_start], 4) == 0) {
+               if (rl_line_buffer[start - 1] == '=') {
+                       matches = rl_completion_matches(text, complete_value);
+               } else {
+                       rl_completion_append_character = '=';
+                       matches = rl_completion_matches(text, complete_line_id);
+               }
+       }
+
+       if ((len == 3 && strncmp("get ", &rl_line_buffer[cmd_start], 4) == 0) ||
+           (len == 6 &&
+            strncmp("toggle ", &rl_line_buffer[cmd_start], 7) == 0))
+               matches = rl_completion_matches(text, complete_line_id);
+
+       return matches;
+}
+
+#define PROMPT "gpioset> "
+
+static void interact(struct gpiod_line_request **requests,
+                    struct line_resolver *resolver, char **lines,
+                    unsigned int *offsets, enum gpiod_line_value *values,
+                    bool unquoted)
+{
+       int num_words, num_lines, max_words, period_us, i;
+       char *line, **words, *line_buf;
+       bool done, stdout_is_tty;
+
+       stifle_history(20);
+       rl_attempted_completion_function = tab_completion;
+       rl_basic_word_break_characters = " =\"";
+       completion_context = resolver;
+       stdout_is_tty = isatty(1);
+
+       max_words = resolver->num_lines + 1;
+       words = calloc(max_words, sizeof(*words));
+       if (!words)
+               die("out of memory");
+
+       for (done = false; !done;) {
+               /*
+                * manually print the prompt, as libedit doesn't if stdout
+                * is not a tty.  And fflush to ensure the prompt and any
+                * output buffered from the previous command is sent.
+                */
+               if (!stdout_is_tty)
+                       printf(PROMPT);
+               fflush(stdout);
+
+               line = readline(PROMPT);
+               if (!line || line[0] == '\0') {
+                       free(line);
+                       continue;
+               }
+
+               for (i = strlen(line) - 1; (i > 0) && isspace(line[i]); i--)
+                       line[i] = '\0';
+
+               line_buf = strdup(line);
+               num_words = split_words(line_buf, max_words, words);
+               if (num_words > max_words) {
+                       printf("too many command parameters provided\n");
+                       goto cmd_done;
+               }
+               num_lines = num_words - 1;
+               if (strcmp(words[0], "get") == 0) {
+                       if (num_lines == 0)
+                               print_all_line_values(resolver, unquoted);
+                       else if (parse_line_ids(num_lines, &words[1], lines) &&
+                                valid_lines(resolver, num_lines, lines))
+                               print_line_values(resolver, num_lines, lines,
+                                                 unquoted);
+                       goto cmd_ok;
+               }
+               if (strcmp(words[0], "set") == 0) {
+                       if (num_lines == 0)
+                               printf("at least one GPIO line value must be specified\n");
+                       else if (parse_line_values(num_lines, &words[1], lines,
+                                                  values, true) &&
+                                valid_lines(resolver, num_lines, lines)) {
+                               set_line_values_subset(resolver, num_lines,
+                                                      lines, values);
+                               apply_values(requests, resolver, offsets,
+                                            values);
+                       }
+                       goto cmd_ok;
+               }
+               if (strcmp(words[0], "toggle") == 0) {
+                       if (num_lines == 0)
+                               toggle_all_lines(resolver);
+                       else if (parse_line_ids(num_lines, &words[1], lines) &&
+                                valid_lines(resolver, num_lines, lines))
+                               toggle_lines(resolver, num_lines, lines);
+
+                       apply_values(requests, resolver, offsets, values);
+                       goto cmd_ok;
+               }
+               if (strcmp(words[0], "sleep") == 0) {
+                       if (num_lines == 0) {
+                               printf("a period must be specified\n");
+                               goto cmd_ok;
+                       }
+                       if (num_lines > 1) {
+                               printf("only one period can be specified\n");
+                               goto cmd_ok;
+                       }
+                       period_us = parse_period(words[1]);
+                       if (period_us < 0) {
+                               printf("invalid period: '%s'\n", words[1]);
+                               goto cmd_ok;
+                       }
+                       usleep(period_us);
+                       goto cmd_ok;
+               }
+
+               if (strcmp(words[0], "exit") == 0) {
+                       done = true;
+                       goto cmd_done;
+               }
+
+               if (strcmp(words[0], "help") == 0) {
+                       print_interactive_help();
+                       goto cmd_done;
+               }
+
+               printf("unknown command: '%s'\n", words[0]);
+               printf("Try the 'help' command\n");
+
+cmd_ok:
+               for (i = 0; isspace(line[i]); i++)
+                       ;
+
+               if ((history_length) == 0 ||
+                   (strcmp(history_list()[history_length - 1]->line,
+                           &line[i]) != 0))
+                       add_history(&line[i]);
+
+cmd_done:
+               free(line);
+               free(line_buf);
+       }
+       free(words);
+}
+
+#endif /* GPIOSET_INTERACTIVE */
+
+int main(int argc, char **argv)
+{
+       struct gpiod_line_settings *settings;
+       struct gpiod_request_config *req_cfg;
+       struct gpiod_line_request **requests;
+       struct gpiod_line_config *line_cfg;
+       struct line_resolver *resolver;
+       enum gpiod_line_value *values;
+       struct gpiod_chip *chip;
+       unsigned int *offsets;
+       int i, num_lines, ret;
+       struct config cfg;
+       char **lines;
+
+       set_prog_name(argv[0]);
+       i = parse_config(argc, argv, &cfg);
+       argc -= i;
+       argv += i;
+
+       if (argc < 1)
+               die("at least one GPIO line value must be specified");
+
+       num_lines = argc;
+
+       lines = calloc(num_lines, sizeof(*lines));
+       values = calloc(num_lines, sizeof(*values));
+       if (!lines || !values)
+               die("out of memory");
+
+       parse_line_values_or_die(argc, argv, lines, values);
+
+       settings = gpiod_line_settings_new();
+       if (!settings)
+               die_perror("unable to allocate line settings");
+
+       if (cfg.bias)
+               gpiod_line_settings_set_bias(settings, cfg.bias);
+
+       if (cfg.drive)
+               gpiod_line_settings_set_drive(settings, cfg.drive);
+
+       if (cfg.active_low)
+               gpiod_line_settings_set_active_low(settings, true);
+
+       gpiod_line_settings_set_direction(settings,
+                                         GPIOD_LINE_DIRECTION_OUTPUT);
+
+       req_cfg = gpiod_request_config_new();
+       if (!req_cfg)
+               die_perror("unable to allocate the request config structure");
+
+       gpiod_request_config_set_consumer(req_cfg, cfg.consumer);
+       resolver = resolve_lines(num_lines, lines, cfg.chip_id, cfg.strict,
+                                cfg.by_name);
+       validate_resolution(resolver, cfg.chip_id);
+       for (i = 0; i < num_lines; i++)
+               resolver->lines[i].value = values[i];
+
+       requests = calloc(resolver->num_chips, sizeof(*requests));
+       offsets = calloc(num_lines, sizeof(*offsets));
+       if (!requests || !offsets)
+               die("out of memory");
+
+       line_cfg = gpiod_line_config_new();
+       if (!line_cfg)
+               die_perror("unable to allocate the line config structure");
+
+       for (i = 0; i < resolver->num_chips; i++) {
+               num_lines = get_line_offsets_and_values(resolver, i, offsets,
+                                                       values);
+
+               gpiod_line_config_reset(line_cfg);
+
+               ret = gpiod_line_config_add_line_settings(line_cfg, offsets,
+                                                         num_lines, settings);
+               if (ret)
+                       die_perror("unable to add line settings");
+
+               ret = gpiod_line_config_set_output_values(line_cfg,
+                                                         values, num_lines);
+               if (ret)
+                       die_perror("unable to set output values");
+
+               chip = gpiod_chip_open(resolver->chips[i].path);
+               if (!chip)
+                       die_perror("unable to open chip '%s'",
+                                  resolver->chips[i].path);
+
+               requests[i] = gpiod_chip_request_lines(chip, req_cfg, line_cfg);
+               if (!requests[i])
+                       die_perror("unable to request lines on chip '%s'",
+                                  resolver->chips[i].path);
+
+               gpiod_chip_close(chip);
+       }
+
+       gpiod_request_config_free(req_cfg);
+       gpiod_line_config_free(line_cfg);
+       gpiod_line_settings_free(settings);
+
+       if (cfg.banner)
+               print_banner(argc, lines);
+
+       if (cfg.daemonize)
+               if (daemon(0, cfg.interactive) < 0)
+                       die_perror("unable to daemonize");
+
+       if (cfg.toggles) {
+               for (i = 0; i < cfg.toggles; i++)
+                       if ((cfg.hold_period_us > cfg.toggle_periods[i]) &&
+                           ((i != cfg.toggles - 1) ||
+                            cfg.toggle_periods[i] != 0))
+                               cfg.toggle_periods[i] = cfg.hold_period_us;
+
+               toggle_sequence(cfg.toggles, cfg.toggle_periods, requests,
+                               resolver, offsets, values);
+               free(cfg.toggle_periods);
+       }
+
+       if (cfg.hold_period_us)
+               usleep(cfg.hold_period_us);
+
+#ifdef GPIOSET_INTERACTIVE
+       if (cfg.interactive)
+               interact(requests, resolver, lines, offsets, values,
+                        cfg.unquoted);
+       else if (!cfg.toggles)
+               wait_fd(gpiod_line_request_get_fd(requests[0]));
+#else
+       if (!cfg.toggles)
+               wait_fd(gpiod_line_request_get_fd(requests[0]));
+#endif
+
+       for (i = 0; i < resolver->num_chips; i++)
+               gpiod_line_request_release(requests[i]);
+
+       free(requests);
+       free_line_resolver(resolver);
+       free(lines);
+       free(values);
+       free(offsets);
+
+       return EXIT_SUCCESS;
+}
diff --git a/tools/tools-common.c b/tools/tools-common.c
new file mode 100644 (file)
index 0000000..64592d3
--- /dev/null
@@ -0,0 +1,783 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+// SPDX-FileCopyrightText: 2017-2021 Bartosz Golaszewski <bartekgola@gmail.com>
+// SPDX-FileCopyrightText: 2022 Kent Gibson <warthog618@gmail.com>
+
+/* Common code for GPIO tools. */
+
+#include <ctype.h>
+#include <dirent.h>
+#include <errno.h>
+#include <gpiod.h>
+#include <inttypes.h>
+#include <limits.h>
+#include <stdarg.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/stat.h>
+#include <time.h>
+
+#include "tools-common.h"
+
+static const char *prog_name = NULL;
+static const char *prog_short_name = NULL;
+
+void set_prog_name(const char *name)
+{
+       prog_name = name;
+       prog_short_name = name;
+       while (*name) {
+               if (*name++ == '/') {
+                       prog_short_name = name;
+               }
+       }
+}
+
+const char *get_prog_name(void)
+{
+       return prog_name;
+}
+
+const char *get_prog_short_name(void)
+{
+       return prog_short_name;
+}
+
+void print_error(const char *fmt, ...)
+{
+       va_list va;
+
+       va_start(va, fmt);
+       fprintf(stderr, "%s: ", get_prog_name());
+       vfprintf(stderr, fmt, va);
+       fprintf(stderr, "\n");
+       va_end(va);
+}
+
+void print_perror(const char *fmt, ...)
+{
+       va_list va;
+
+       va_start(va, fmt);
+       fprintf(stderr, "%s: ", get_prog_name());
+       vfprintf(stderr, fmt, va);
+       fprintf(stderr, ": %s\n", strerror(errno));
+       va_end(va);
+}
+
+void die(const char *fmt, ...)
+{
+       va_list va;
+
+       va_start(va, fmt);
+       fprintf(stderr, "%s: ", get_prog_name());
+       vfprintf(stderr, fmt, va);
+       fprintf(stderr, "\n");
+       va_end(va);
+
+       exit(EXIT_FAILURE);
+}
+
+void die_perror(const char *fmt, ...)
+{
+       va_list va;
+
+       va_start(va, fmt);
+       fprintf(stderr, "%s: ", get_prog_name());
+       vfprintf(stderr, fmt, va);
+       fprintf(stderr, ": %s\n", strerror(errno));
+       va_end(va);
+
+       exit(EXIT_FAILURE);
+}
+
+void print_version(void)
+{
+       printf("%s (libgpiod) v%s\n", get_prog_short_name(), gpiod_api_version());
+       printf("Copyright (C) 2017-2023 Bartosz Golaszewski\n");
+       printf("License: GPL-2.0-or-later\n");
+       printf("This is free software: you are free to change and redistribute it.\n");
+       printf("There is NO WARRANTY, to the extent permitted by law.\n");
+}
+
+int parse_bias_or_die(const char *option)
+{
+       if (strcmp(option, "pull-down") == 0)
+               return GPIOD_LINE_BIAS_PULL_DOWN;
+       if (strcmp(option, "pull-up") == 0)
+               return GPIOD_LINE_BIAS_PULL_UP;
+       if (strcmp(option, "disabled") != 0)
+               die("invalid bias: %s", option);
+
+       return GPIOD_LINE_BIAS_DISABLED;
+}
+
+int parse_period(const char *option)
+{
+       unsigned long p, m = 0;
+       char *end;
+
+       p = strtoul(option, &end, 10);
+
+       switch (*end) {
+       case 'u':
+               m = 1;
+               end++;
+               break;
+       case 'm':
+               m = 1000;
+               end++;
+               break;
+       case 's':
+               m = 1000000;
+               break;
+       case '\0':
+               break;
+       default:
+               return -1;
+       }
+
+       if (m) {
+               if (*end != 's')
+                       return -1;
+
+               end++;
+       } else {
+               m = 1000;
+       }
+
+       p *= m;
+       if (*end != '\0' || p > INT_MAX)
+               return -1;
+
+       return p;
+}
+
+unsigned int parse_period_or_die(const char *option)
+{
+       int period = parse_period(option);
+
+       if (period < 0)
+               die("invalid period: %s", option);
+
+       return period;
+}
+
+int parse_uint(const char *option)
+{
+       unsigned long o;
+       char *end;
+
+       o = strtoul(option, &end, 10);
+       if (*end == '\0' && o <= INT_MAX)
+               return o;
+
+       return -1;
+}
+
+unsigned int parse_uint_or_die(const char *option)
+{
+       int i = parse_uint(option);
+
+       if (i < 0)
+               die("invalid number: '%s'", option);
+
+       return i;
+}
+
+void print_bias_help(void)
+{
+       printf("  -b, --bias <bias>\tspecify the line bias\n");
+       printf("\t\t\tPossible values: 'pull-down', 'pull-up', 'disabled'.\n");
+       printf("\t\t\t(default is to leave bias unchanged)\n");
+}
+
+void print_chip_help(void)
+{
+       printf("\nChips:\n");
+       printf("    A GPIO chip may be identified by number, name, or path.\n");
+       printf("    e.g. '0', 'gpiochip0', and '/dev/gpiochip0' all refer to the same chip.\n");
+}
+
+void print_period_help(void)
+{
+       printf("\nPeriods:\n");
+       printf("    Periods are taken as milliseconds unless units are specified. e.g. 10us.\n");
+       printf("    Supported units are 's', 'ms', and 'us'.\n");
+}
+
+#define TIME_BUFFER_SIZE 20
+
+/*
+ * format:
+ * 0: raw seconds
+ * 1: utc time
+ * 2: local time
+ */
+void print_event_time(uint64_t evtime, int format)
+{
+       char tbuf[TIME_BUFFER_SIZE];
+       time_t evtsec;
+       struct tm t;
+       char *tz;
+
+       if (format) {
+               evtsec = evtime / 1000000000;
+               if (format == 2) {
+                       localtime_r(&evtsec, &t);
+                       tz = "";
+               } else {
+                       gmtime_r(&evtsec, &t);
+                       tz = "Z";
+               }
+               strftime(tbuf, TIME_BUFFER_SIZE, "%FT%T", &t);
+               printf("%s.%09" PRIu64 "%s", tbuf, evtime % 1000000000, tz);
+       } else {
+               printf("%" PRIu64 ".%09" PRIu64, evtime / 1000000000,
+                      evtime % 1000000000);
+       }
+}
+
+static void print_bias(struct gpiod_line_info *info)
+{
+       const char *name;
+
+       switch (gpiod_line_info_get_bias(info)) {
+       case GPIOD_LINE_BIAS_PULL_UP:
+               name = "pull-up";
+               break;
+       case GPIOD_LINE_BIAS_PULL_DOWN:
+               name = "pull-down";
+               break;
+       case GPIOD_LINE_BIAS_DISABLED:
+               name = "disabled";
+               break;
+       default:
+               return;
+       }
+
+       printf(" bias=%s", name);
+}
+
+static void print_drive(struct gpiod_line_info *info)
+{
+       const char *name;
+
+       switch (gpiod_line_info_get_drive(info)) {
+       case GPIOD_LINE_DRIVE_OPEN_DRAIN:
+               name = "open-drain";
+               break;
+       case GPIOD_LINE_DRIVE_OPEN_SOURCE:
+               name = "open-source";
+               break;
+       default:
+               return;
+       }
+
+       printf(" drive=%s", name);
+}
+
+static void print_edge_detection(struct gpiod_line_info *info)
+{
+       const char *name;
+
+       switch (gpiod_line_info_get_edge_detection(info)) {
+       case GPIOD_LINE_EDGE_BOTH:
+               name = "both";
+               break;
+       case GPIOD_LINE_EDGE_RISING:
+               name = "rising";
+               break;
+       case GPIOD_LINE_EDGE_FALLING:
+               name = "falling";
+               break;
+       default:
+               return;
+       }
+
+       printf(" edges=%s", name);
+}
+
+static void print_event_clock(struct gpiod_line_info *info)
+{
+       const char *name;
+
+       switch (gpiod_line_info_get_event_clock(info)) {
+       case GPIOD_LINE_CLOCK_REALTIME:
+               name = "realtime";
+               break;
+       case GPIOD_LINE_CLOCK_HTE:
+               name = "hte";
+               break;
+       default:
+               return;
+       }
+
+       printf(" event-clock=%s", name);
+}
+
+static void print_debounce(struct gpiod_line_info *info)
+{
+       const char *units = "us";
+       unsigned long debounce;
+
+       debounce = gpiod_line_info_get_debounce_period_us(info);
+       if (!debounce)
+               return;
+       if (debounce % 1000000 == 0) {
+               debounce /= 1000000;
+               units = "s";
+       } else if (debounce % 1000 == 0) {
+               debounce /= 1000;
+               units = "ms";
+       }
+       printf(" debounce-period=%lu%s", debounce, units);
+}
+
+static void print_consumer(struct gpiod_line_info *info, bool unquoted)
+{
+       const char *consumer;
+       const char *fmt;
+
+       if (!gpiod_line_info_is_used(info))
+               return;
+
+       consumer = gpiod_line_info_get_consumer(info);
+       if (!consumer)
+               consumer = "kernel";
+
+       fmt = unquoted ? " consumer=%s" : " consumer=\"%s\"";
+
+       printf(fmt, consumer);
+}
+
+void print_line_attributes(struct gpiod_line_info *info, bool unquoted_strings)
+{
+       enum gpiod_line_direction direction;
+
+       direction = gpiod_line_info_get_direction(info);
+
+       printf("%s", direction == GPIOD_LINE_DIRECTION_INPUT ?
+                       "input" : "output");
+
+       if (gpiod_line_info_is_active_low(info))
+               printf(" active-low");
+
+       print_drive(info);
+       print_bias(info);
+       print_edge_detection(info);
+       print_event_clock(info);
+       print_debounce(info);
+       print_consumer(info, unquoted_strings);
+}
+
+void print_line_id(struct line_resolver *resolver, int chip_num,
+                  unsigned int offset, const char *chip_id, bool unquoted)
+{
+       const char *lname, *fmt;
+
+       lname = get_line_name(resolver, chip_num, offset);
+       if (!lname) {
+               printf("%s %u", get_chip_name(resolver, chip_num), offset);
+               return;
+       }
+       if (chip_id)
+               printf("%s %u ", get_chip_name(resolver, chip_num), offset);
+
+       fmt = unquoted ? "%s" : "\"%s\"";
+       printf(fmt, lname);
+}
+
+static int chip_dir_filter(const struct dirent *entry)
+{
+       struct stat sb;
+       int ret = 0;
+       char *path;
+
+       if (asprintf(&path, "/dev/%s", entry->d_name) < 0)
+               return 0;
+
+       if ((lstat(path, &sb) == 0) && (!S_ISLNK(sb.st_mode)) &&
+           gpiod_is_gpiochip_device(path))
+               ret = 1;
+
+       free(path);
+
+       return ret;
+}
+
+static bool isuint(const char *str)
+{
+       for (; *str && isdigit(*str); str++)
+               ;
+
+       return *str == '\0';
+}
+
+bool chip_path_lookup(const char *id, char **path_ptr)
+{
+       char *path;
+
+       if (isuint(id)) {
+               /* by number */
+               if (asprintf(&path, "/dev/gpiochip%s", id) < 0)
+                       return false;
+       } else if (strchr(id, '/')) {
+               /* by path */
+               if (asprintf(&path, "%s", id) < 0)
+                       return false;
+       } else {
+               /* by device name */
+               if (asprintf(&path, "/dev/%s", id) < 0)
+                       return false;
+       }
+
+       if (!gpiod_is_gpiochip_device(path)) {
+               free(path);
+               return false;
+       }
+
+       *path_ptr = path;
+
+       return true;
+}
+
+int chip_paths(const char *id, char ***paths_ptr)
+{
+       char **paths;
+       char *path;
+
+       if (id == NULL)
+               return all_chip_paths(paths_ptr);
+
+       if (!chip_path_lookup(id, &path))
+               return 0;
+
+       paths = malloc(sizeof(*paths));
+       if (paths == NULL)
+               die("out of memory");
+
+       paths[0] = path;
+       *paths_ptr = paths;
+
+       return 1;
+}
+
+int all_chip_paths(char ***paths_ptr)
+{
+       int i, j, num_chips, ret = 0;
+       struct dirent **entries;
+       char **paths;
+
+       num_chips = scandir("/dev/", &entries, chip_dir_filter, versionsort);
+       if (num_chips < 0)
+               die_perror("unable to scan /dev");
+
+       paths = calloc(num_chips, sizeof(*paths));
+       if (paths == NULL)
+               die("out of memory");
+
+       for (i = 0; i < num_chips; i++) {
+               if (asprintf(&paths[i], "/dev/%s", entries[i]->d_name) < 0) {
+                       for (j = 0; j < i; j++)
+                               free(paths[j]);
+
+                       free(paths);
+                       return 0;
+               }
+       }
+
+       *paths_ptr = paths;
+       ret = num_chips;
+
+       for (i = 0; i < num_chips; i++)
+               free(entries[i]);
+
+       free(entries);
+       return ret;
+}
+
+static bool resolve_line(struct line_resolver *resolver,
+                        struct gpiod_line_info *info, int chip_num)
+{
+       struct resolved_line *line;
+       bool resolved = false;
+       unsigned int offset;
+       const char *name;
+       int i;
+
+       offset = gpiod_line_info_get_offset(info);
+       for (i = 0; i < resolver->num_lines; i++) {
+               line = &resolver->lines[i];
+               /* already resolved by offset? */
+               if (line->resolved && (line->offset == offset) &&
+                   (line->chip_num == chip_num)) {
+                       line->info = info;
+                       resolver->num_found++;
+                       resolved = true;
+               }
+               if (line->resolved && !resolver->strict)
+                       continue;
+
+               /* else resolve by name */
+               name = gpiod_line_info_get_name(info);
+               if (name && (strcmp(line->id, name) == 0)) {
+                       if (resolver->strict && line->resolved)
+                               die("line '%s' is not unique", line->id);
+                       line->offset = offset;
+                       line->info = info;
+                       line->chip_num = resolver->num_chips;
+                       line->resolved = true;
+                       resolver->num_found++;
+                       resolved = true;
+               }
+       }
+
+       return resolved;
+}
+
+/*
+ * check for lines that can be identified by offset
+ *
+ * This only applies to the first chip, as otherwise the lines must be
+ * identified by name.
+ */
+bool resolve_lines_by_offset(struct line_resolver *resolver,
+                            unsigned int num_lines)
+{
+       struct resolved_line *line;
+       bool used = false;
+       int i;
+
+       for (i = 0; i < resolver->num_lines; i++) {
+               line = &resolver->lines[i];
+               if ((line->id_as_offset != -1) &&
+                   (line->id_as_offset < (int)num_lines)) {
+                       line->chip_num = 0;
+                       line->offset = line->id_as_offset;
+                       line->resolved = true;
+                       used = true;
+               }
+       }
+       return used;
+}
+
+bool resolve_done(struct line_resolver *resolver)
+{
+       return (!resolver->strict &&
+               resolver->num_found >= resolver->num_lines);
+}
+
+struct line_resolver *resolver_init(int num_lines, char **lines, int num_chips,
+                                   bool strict, bool by_name)
+{
+       struct line_resolver *resolver;
+       struct resolved_line *line;
+       size_t resolver_size;
+       int i;
+
+       resolver_size = sizeof(*resolver) + num_lines * sizeof(*line);
+       resolver = malloc(resolver_size);
+       if (resolver == NULL)
+               die("out of memory");
+
+       memset(resolver, 0, resolver_size);
+
+       resolver->chips = calloc(num_chips, sizeof(struct resolved_chip));
+       if (resolver->chips == NULL)
+               die("out of memory");
+       memset(resolver->chips, 0, num_chips * sizeof(struct resolved_chip));
+
+       resolver->num_lines = num_lines;
+       resolver->strict = strict;
+       for (i = 0; i < num_lines; i++) {
+               line = &resolver->lines[i];
+               line->id = lines[i];
+               line->id_as_offset = by_name ? -1 : parse_uint(lines[i]);
+               line->chip_num = -1;
+       }
+
+       return resolver;
+}
+
+struct line_resolver *resolve_lines(int num_lines, char **lines,
+                                   const char *chip_id, bool strict,
+                                   bool by_name)
+{
+       struct gpiod_chip_info *chip_info;
+       struct gpiod_line_info *line_info;
+       struct line_resolver *resolver;
+       int num_chips, i, offset;
+       struct gpiod_chip *chip;
+       bool chip_used;
+       char **paths;
+
+       if (chip_id == NULL)
+               by_name = true;
+
+       num_chips = chip_paths(chip_id, &paths);
+       if (chip_id && (num_chips == 0))
+               die("cannot find GPIO chip character device '%s'", chip_id);
+
+       resolver = resolver_init(num_lines, lines, num_chips, strict, by_name);
+
+       for (i = 0; (i < num_chips) && !resolve_done(resolver); i++) {
+               chip_used = false;
+               chip = gpiod_chip_open(paths[i]);
+               if (!chip) {
+                       if ((errno == EACCES) && (chip_id == NULL)) {
+                               free(paths[i]);
+                               continue;
+                       }
+
+                       die_perror("unable to open chip '%s'", paths[i]);
+               }
+
+               chip_info = gpiod_chip_get_info(chip);
+               if (!chip_info)
+                       die_perror("unable to get info for '%s'", paths[i]);
+
+               num_lines = gpiod_chip_info_get_num_lines(chip_info);
+
+               if (i == 0 && chip_id && !by_name)
+                       chip_used = resolve_lines_by_offset(resolver, num_lines);
+
+               for (offset = 0;
+                    (offset < num_lines) && !resolve_done(resolver);
+                    offset++) {
+                       line_info = gpiod_chip_get_line_info(chip, offset);
+                       if (!line_info)
+                               die_perror("unable to read the info for line %d from %s",
+                                          offset,
+                                          gpiod_chip_info_get_name(chip_info));
+
+                       if (resolve_line(resolver, line_info, i))
+                               chip_used = true;
+                       else
+                               gpiod_line_info_free(line_info);
+
+               }
+
+               gpiod_chip_close(chip);
+
+               if (chip_used) {
+                       resolver->chips[resolver->num_chips].info = chip_info;
+                       resolver->chips[resolver->num_chips].path = paths[i];
+                       resolver->num_chips++;
+               } else {
+                       gpiod_chip_info_free(chip_info);
+                       free(paths[i]);
+               }
+       }
+       free(paths);
+
+       return resolver;
+}
+
+void validate_resolution(struct line_resolver *resolver, const char *chip_id)
+{
+       struct resolved_line *line, *prev;
+       bool valid = true;
+       int i, j;
+
+       for (i = 0; i < resolver->num_lines; i++) {
+               line = &resolver->lines[i];
+               if (line->resolved) {
+                       for (j = 0; j < i; j++) {
+                               prev = &resolver->lines[j];
+                               if (prev->resolved &&
+                                   (prev->chip_num == line->chip_num) &&
+                                   (prev->offset == line->offset)) {
+                                       print_error("lines '%s' and '%s' are the same line",
+                                                   prev->id, line->id);
+                                       valid = false;
+                                       break;
+                               }
+                       }
+                       continue;
+               }
+               valid = false;
+               if (chip_id && line->id_as_offset != -1)
+                       print_error("offset %s is out of range on chip '%s'",
+                                   line->id, chip_id);
+               else
+                       print_error("cannot find line '%s'", line->id);
+       }
+       if (!valid)
+               exit(EXIT_FAILURE);
+}
+
+void free_line_resolver(struct line_resolver *resolver)
+{
+       int i;
+
+       if (!resolver)
+               return;
+
+       for (i = 0; i < resolver->num_lines; i++)
+               gpiod_line_info_free(resolver->lines[i].info);
+
+       for (i = 0; i < resolver->num_chips; i++) {
+               gpiod_chip_info_free(resolver->chips[i].info);
+               free(resolver->chips[i].path);
+       }
+
+       free(resolver->chips);
+       free(resolver);
+}
+
+int get_line_offsets_and_values(struct line_resolver *resolver, int chip_num,
+                               unsigned int *offsets,
+                               enum gpiod_line_value *values)
+{
+       struct resolved_line *line;
+       int i, num_lines = 0;
+
+       for (i = 0; i < resolver->num_lines; i++) {
+               line = &resolver->lines[i];
+               if (line->chip_num == chip_num) {
+                       offsets[num_lines] = line->offset;
+                       if (values)
+                               values[num_lines] = line->value;
+
+                       num_lines++;
+               }
+       }
+
+       return num_lines;
+}
+
+const char *get_chip_name(struct line_resolver *resolver, int chip_num)
+{
+       return gpiod_chip_info_get_name(resolver->chips[chip_num].info);
+}
+
+const char *get_line_name(struct line_resolver *resolver, int chip_num,
+                         unsigned int offset)
+{
+       struct resolved_line *line;
+       int i;
+
+       for (i = 0; i < resolver->num_lines; i++) {
+               line = &resolver->lines[i];
+               if (line->info && (line->offset == offset) &&
+                   (line->chip_num == chip_num))
+                       return gpiod_line_info_get_name(
+                               resolver->lines[i].info);
+       }
+
+       return 0;
+}
+
+void set_line_values(struct line_resolver *resolver, int chip_num,
+                    enum gpiod_line_value *values)
+{
+       int i, j;
+
+       for (i = 0, j = 0; i < resolver->num_lines; i++) {
+               if (resolver->lines[i].chip_num == chip_num) {
+                       resolver->lines[i].value = values[j];
+                       j++;
+               }
+       }
+}
diff --git a/tools/tools-common.h b/tools/tools-common.h
new file mode 100644 (file)
index 0000000..c82317a
--- /dev/null
@@ -0,0 +1,123 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/* SPDX-FileCopyrightText: 2017-2021 Bartosz Golaszewski <bartekgola@gmail.com> */
+/* SPDX-FileCopyrightText: 2022 Kent Gibson <warthog618@gmail.com> */
+
+#ifndef __GPIOD_TOOLS_COMMON_H__
+#define __GPIOD_TOOLS_COMMON_H__
+
+#include <gpiod.h>
+
+/*
+ * Various helpers for the GPIO tools.
+ *
+ * NOTE: This is not a stable interface - it's only to avoid duplicating
+ * common code.
+ */
+
+#define NORETURN               __attribute__((noreturn))
+#define PRINTF(fmt, arg)       __attribute__((format(printf, fmt, arg)))
+
+#define GETOPT_NULL_LONGOPT    NULL, 0, NULL, 0
+
+struct resolved_line {
+       /* from the command line */
+       const char *id;
+
+       /*
+        * id parsed as int, if that is an option, or -1 if line must be
+        * resolved by name
+        */
+       int id_as_offset;
+
+       /* line has been located on a chip */
+       bool resolved;
+
+       /* remaining fields only valid once resolved... */
+
+       /* info for the line */
+       struct gpiod_line_info *info;
+
+       /* num of relevant chip in line_resolver */
+       int chip_num;
+
+       /* offset of line on chip */
+       unsigned int offset;
+
+       /* line value for gpioget/set */
+       int value;
+};
+
+struct resolved_chip {
+       /* info of the relevant chips */
+       struct gpiod_chip_info *info;
+
+       /* path to the chip */
+       char *path;
+};
+
+/* a resolver from requested line names/offsets to lines on the system */
+struct line_resolver {
+       /*
+        * number of chips the lines span, and number of entries in chips
+        */
+       int num_chips;
+
+       /* number of lines in lines */
+       int num_lines;
+
+       /* number of lines found */
+       int num_found;
+
+       /* perform exhaustive search to check line names are unique */
+       bool strict;
+
+       /* details of the relevant chips */
+       struct resolved_chip *chips;
+
+       /* descriptors for the requested lines */
+       struct resolved_line lines[];
+};
+
+void set_prog_name(const char *name);
+const char *get_prog_name(void);
+const char *get_prog_short_name(void);
+void print_error(const char *fmt, ...) PRINTF(1, 2);
+void print_perror(const char *fmt, ...) PRINTF(1, 2);
+void die(const char *fmt, ...) NORETURN PRINTF(1, 2);
+void die_perror(const char *fmt, ...) NORETURN PRINTF(1, 2);
+void print_version(void);
+int parse_bias_or_die(const char *option);
+int parse_period(const char *option);
+unsigned int parse_period_or_die(const char *option);
+int parse_uint(const char *option);
+unsigned int parse_uint_or_die(const char *option);
+void print_bias_help(void);
+void print_chip_help(void);
+void print_period_help(void);
+void print_event_time(uint64_t evtime, int format);
+void print_line_attributes(struct gpiod_line_info *info, bool unquoted_strings);
+void print_line_id(struct line_resolver *resolver, int chip_num,
+                  unsigned int offset, const char *chip_id, bool unquoted);
+bool chip_path_lookup(const char *id, char **path_ptr);
+int chip_paths(const char *id, char ***paths_ptr);
+int all_chip_paths(char ***paths_ptr);
+struct line_resolver *resolve_lines(int num_lines, char **lines,
+                                   const char *chip_id, bool strict,
+                                   bool by_name);
+struct line_resolver *resolver_init(int num_lines, char **lines, int num_chips,
+                                   bool strict, bool by_name);
+bool resolve_lines_by_offset(struct line_resolver *resolver,
+                            unsigned int num_lines);
+bool resolve_done(struct line_resolver *resolver);
+void validate_resolution(struct line_resolver *resolver, const char *chip_id);
+void free_line_resolver(struct line_resolver *resolver);
+int get_line_offsets_and_values(struct line_resolver *resolver, int chip_num,
+                               unsigned int *offsets,
+                               enum gpiod_line_value *values);
+const char *get_chip_name(struct line_resolver *resolver, int chip_num);
+const char *get_line_name(struct line_resolver *resolver, int chip_num,
+                         unsigned int offset);
+void set_line_values(struct line_resolver *resolver, int chip_num,
+                    enum gpiod_line_value *values);
+
+#endif /* __GPIOD_TOOLS_COMMON_H__ */