From: Gavin Lai (賴建宇) Date: Sun, 18 Aug 2024 08:17:42 +0000 (+0800) Subject: Import libgpiod_2.1.3.orig.tar.xz X-Git-Tag: archive/raspbian/2.1.3-1+rpi1^2~4 X-Git-Url: https://dgit.raspbian.org/?a=commitdiff_plain;h=5e45c84ae75e0c4e297316dc94facbc33dae3dc3;p=libgpiod.git Import libgpiod_2.1.3.orig.tar.xz [dgit import orig libgpiod_2.1.3.orig.tar.xz] --- 5e45c84ae75e0c4e297316dc94facbc33dae3dc3 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6c08415 --- /dev/null +++ b/.gitignore @@ -0,0 +1,37 @@ +# SPDX-License-Identifier: GPL-2.0-or-later +# SPDX-FileCopyrightText: 2017-2021 Bartosz Golaszewski + +*.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 index 0000000..f40e95f --- /dev/null +++ b/.readthedocs.yaml @@ -0,0 +1,27 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +# SPDX-FileCopyrightText: 2022 Kent Gibson + +# +# 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 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 index 0000000..9c85e21 --- /dev/null +++ b/Doxyfile.in @@ -0,0 +1,105 @@ +# SPDX-License-Identifier: GPL-2.0-or-later +# SPDX-FileCopyrightText: 2017-2021 Bartosz Golaszewski + +# 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 index 0000000..261eeb9 --- /dev/null +++ b/LICENSES/Apache-2.0.txt @@ -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 index 0000000..ddd44f6 --- /dev/null +++ b/LICENSES/BSD-3-Clause.txt @@ -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 index 0000000..3b7b82d --- /dev/null +++ b/LICENSES/CC-BY-SA-4.0.txt @@ -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 index 0000000..0e259d4 --- /dev/null +++ b/LICENSES/CC0-1.0.txt @@ -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 index 0000000..d159169 --- /dev/null +++ b/LICENSES/GPL-2.0-only.txt @@ -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. + + + Copyright (C) + + 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. + + , 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 index 0000000..0a87fbd --- /dev/null +++ b/LICENSES/GPL-2.0-or-later.txt @@ -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 index 0000000..e5ab03e --- /dev/null +++ b/LICENSES/LGPL-2.1-or-later.txt @@ -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. + + + Copyright (C) + + 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. + + , 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 index 0000000..9abdad7 --- /dev/null +++ b/LICENSES/Linux-syscall-note.txt @@ -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: 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 index 0000000..2ace901 --- /dev/null +++ b/Makefile.am @@ -0,0 +1,51 @@ +# SPDX-License-Identifier: GPL-2.0-or-later +# SPDX-FileCopyrightText: 2017-2021 Bartosz Golaszewski + +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 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 +# SPDX-FileCopyrightText: 2023 Bartosz Golaszewski + +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 instead of 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 for struct timespec +- include instead of +- 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 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 + +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= + 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 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 + +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 index 0000000..420b821 --- /dev/null +++ b/autogen.sh @@ -0,0 +1,17 @@ +#!/bin/sh +# SPDX-License-Identifier: GPL-2.0-or-later +# SPDX-FileCopyrightText: 2017-2021 Bartosz Golaszewski +# SPDX-FileCopyrightText: 2017 Thierry Reding + +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 index 0000000..004ae23 --- /dev/null +++ b/bindings/Makefile.am @@ -0,0 +1,22 @@ +# SPDX-License-Identifier: GPL-2.0-or-later +# SPDX-FileCopyrightText: 2017-2021 Bartosz Golaszewski + +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 index 0000000..e2a89cf --- /dev/null +++ b/bindings/cxx/Makefile.am @@ -0,0 +1,47 @@ +# SPDX-License-Identifier: GPL-2.0-or-later +# SPDX-FileCopyrightText: 2017-2021 Bartosz Golaszewski + +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 index 0000000..93dd6f5 --- /dev/null +++ b/bindings/cxx/chip-info.cpp @@ -0,0 +1,77 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later +// SPDX-FileCopyrightText: 2021-2022 Bartosz Golaszewski + +#include +#include + +#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 index 0000000..8086839 --- /dev/null +++ b/bindings/cxx/chip.cpp @@ -0,0 +1,202 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later +// SPDX-FileCopyrightText: 2021-2022 Bartosz Golaszewski + +#include +#include + +#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 index 0000000..0d5cb36 --- /dev/null +++ b/bindings/cxx/edge-event-buffer.cpp @@ -0,0 +1,117 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later +// SPDX-FileCopyrightText: 2021-2022 Bartosz Golaszewski + +#include +#include +#include + +#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(*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(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 index 0000000..9ef311c --- /dev/null +++ b/bindings/cxx/edge-event.cpp @@ -0,0 +1,137 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later +// SPDX-FileCopyrightText: 2021-2022 Bartosz Golaszewski + +#include +#include +#include + +#include "internal.hpp" + +namespace gpiod { + +namespace { + +const ::std::map 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 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_managed::copy(const ::std::shared_ptr& 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_external::copy([[maybe_unused]] const ::std::shared_ptr& self) const +{ + ::std::shared_ptr ret(new impl_managed); + impl_managed& managed = dynamic_cast(*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 index 0000000..0f9b39e --- /dev/null +++ b/bindings/cxx/examples/.gitignore @@ -0,0 +1,16 @@ +# SPDX-License-Identifier: GPL-2.0-or-later +# SPDX-FileCopyrightText: 2017-2021 Bartosz Golaszewski + +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 index 0000000..eca4d64 --- /dev/null +++ b/bindings/cxx/examples/Makefile.am @@ -0,0 +1,47 @@ +# SPDX-License-Identifier: GPL-2.0-or-later +# SPDX-FileCopyrightText: 2017-2021 Bartosz Golaszewski + +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 index 0000000..9ea9763 --- /dev/null +++ b/bindings/cxx/examples/async_watch_line_value.cpp @@ -0,0 +1,88 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +// SPDX-FileCopyrightText: 2023 Kent Gibson + +/* Minimal example of asynchronously watching for edges on a single line. */ + +#include +#include +#include +#include +#include +#include +#include + +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 index 0000000..7b56e69 --- /dev/null +++ b/bindings/cxx/examples/find_line_by_name.cpp @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +// SPDX-FileCopyrightText: 2023 Kent Gibson + +/* Minimal example of finding a line with the given name. */ + +#include +#include +#include +#include + +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 index 0000000..2bf26f0 --- /dev/null +++ b/bindings/cxx/examples/get_chip_info.cpp @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +// SPDX-FileCopyrightText: 2023 Kent Gibson + +/* Minimal example of reading the info for a chip. */ + +#include +#include +#include +#include + +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 index 0000000..7d517af --- /dev/null +++ b/bindings/cxx/examples/get_line_info.cpp @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +// SPDX-FileCopyrightText: 2023 Kent Gibson + +/* Minimal example of reading the info for a line. */ + +#include +#include +#include +#include +#include + +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 index 0000000..fe4f52e --- /dev/null +++ b/bindings/cxx/examples/get_line_value.cpp @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +// SPDX-FileCopyrightText: 2023 Kent Gibson + +/* Minimal example of reading a single line. */ + +#include +#include +#include +#include + +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 index 0000000..cbd5395 --- /dev/null +++ b/bindings/cxx/examples/get_multiple_line_values.cpp @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +// SPDX-FileCopyrightText: 2023 Kent Gibson + +/* Minimal example of reading multiple lines. */ + +#include +#include +#include + +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 index 0000000..d55eaf5 --- /dev/null +++ b/bindings/cxx/examples/reconfigure_input_to_output.cpp @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +// SPDX-FileCopyrightText: 2023 Kent Gibson + +/* + * Example of a bi-directional line requested as input and then switched + * to output. + */ + +#include +#include +#include +#include + +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 index 0000000..3dbdb71 --- /dev/null +++ b/bindings/cxx/examples/toggle_line_value.cpp @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +// SPDX-FileCopyrightText: 2023 Kent Gibson + +/* Minimal example of toggling a single line. */ + +#include +#include +#include +#include +#include +#include + +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 index 0000000..df55313 --- /dev/null +++ b/bindings/cxx/examples/toggle_multiple_line_values.cpp @@ -0,0 +1,63 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +// SPDX-FileCopyrightText: 2023 Kent Gibson + +/* Minimal example of toggling multiple lines. */ + +#include +#include +#include +#include + +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 index 0000000..6d55500 --- /dev/null +++ b/bindings/cxx/examples/watch_line_info.cpp @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +// SPDX-FileCopyrightText: 2023 Kent Gibson + +/* Minimal example of watching for requests on particular lines. */ + +#include +#include +#include +#include + +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 index 0000000..33e4f01 --- /dev/null +++ b/bindings/cxx/examples/watch_line_rising.cpp @@ -0,0 +1,64 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +// SPDX-FileCopyrightText: 2023 Kent Gibson + +/* Minimal example of watching for rising edges on a single line. */ + +#include +#include +#include +#include +#include + +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 index 0000000..ebc7fe7 --- /dev/null +++ b/bindings/cxx/examples/watch_line_value.cpp @@ -0,0 +1,71 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +// SPDX-FileCopyrightText: 2023 Kent Gibson + +/* Minimal example of watching for edges on a single line. */ + +#include +#include +#include +#include +#include + +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 index 0000000..fb71fb2 --- /dev/null +++ b/bindings/cxx/examples/watch_multiple_line_values.cpp @@ -0,0 +1,60 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +// SPDX-FileCopyrightText: 2023 Kent Gibson + +/* Minimal example of watching for edges on multiple lines. */ + +#include +#include +#include +#include + +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 index 0000000..378b249 --- /dev/null +++ b/bindings/cxx/exception.cpp @@ -0,0 +1,119 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later +// SPDX-FileCopyrightText: 2021-2022 Bartosz Golaszewski + +#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 index 0000000..91e41a5 --- /dev/null +++ b/bindings/cxx/gpiod.hpp @@ -0,0 +1,48 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +/* SPDX-FileCopyrightText: 2021-2022 Bartosz Golaszewski */ + +/** + * @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 index 0000000..e3a3b9b --- /dev/null +++ b/bindings/cxx/gpiodcxx/Makefile.am @@ -0,0 +1,20 @@ +# SPDX-License-Identifier: GPL-2.0-or-later +# SPDX-FileCopyrightText: 2021 Bartosz Golaszewski + +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 index 0000000..e740e94 --- /dev/null +++ b/bindings/cxx/gpiodcxx/chip-info.hpp @@ -0,0 +1,105 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +/* SPDX-FileCopyrightText: 2022 Bartosz Golaszewski */ + +/** + * @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 +#include + +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 _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 index 0000000..39e0318 --- /dev/null +++ b/bindings/cxx/gpiodcxx/chip.hpp @@ -0,0 +1,182 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +/* SPDX-FileCopyrightText: 2021-2022 Bartosz Golaszewski */ + +/** + * @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 +#include +#include +#include +#include + +#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 _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 index 0000000..2482e8a --- /dev/null +++ b/bindings/cxx/gpiodcxx/edge-event-buffer.hpp @@ -0,0 +1,129 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +/* SPDX-FileCopyrightText: 2021-2022 Bartosz Golaszewski */ + +/** + * @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 +#include +#include +#include + +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::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 _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 index 0000000..47c256a --- /dev/null +++ b/bindings/cxx/gpiodcxx/edge-event.hpp @@ -0,0 +1,137 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +/* SPDX-FileCopyrightText: 2021-2022 Bartosz Golaszewski */ + +/** + * @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 +#include +#include + +#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 _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 index 0000000..34737d2 --- /dev/null +++ b/bindings/cxx/gpiodcxx/exception.hpp @@ -0,0 +1,158 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +/* SPDX-FileCopyrightText: 2021-2022 Bartosz Golaszewski */ + +/** + * @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 +#include + +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 index 0000000..69b88b6 --- /dev/null +++ b/bindings/cxx/gpiodcxx/info-event.hpp @@ -0,0 +1,123 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +/* SPDX-FileCopyrightText: 2021-2022 Bartosz Golaszewski */ + +/** + * @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 +#include +#include + +#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 _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 index 0000000..58c9d87 --- /dev/null +++ b/bindings/cxx/gpiodcxx/line-config.hpp @@ -0,0 +1,120 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +/* SPDX-FileCopyrightText: 2021-2022 Bartosz Golaszewski */ + +/** + * @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 +#include + +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 get_line_settings() const; + +private: + + struct impl; + + ::std::shared_ptr _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 index 0000000..8b10dc4 --- /dev/null +++ b/bindings/cxx/gpiodcxx/line-info.hpp @@ -0,0 +1,176 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +/* SPDX-FileCopyrightText: 2021-2022 Bartosz Golaszewski */ + +/** + * @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 +#include +#include +#include + +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 _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 index 0000000..a658825 --- /dev/null +++ b/bindings/cxx/gpiodcxx/line-request.hpp @@ -0,0 +1,236 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +/* SPDX-FileCopyrightText: 2021-2022 Bartosz Golaszewski */ + +/** + * @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 +#include +#include +#include + +#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 _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 index 0000000..1004ccd --- /dev/null +++ b/bindings/cxx/gpiodcxx/line-settings.hpp @@ -0,0 +1,202 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +/* SPDX-FileCopyrightText: 2022 Bartosz Golaszewski */ + +/** + * @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 +#include + +#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 _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 index 0000000..2810571 --- /dev/null +++ b/bindings/cxx/gpiodcxx/line.hpp @@ -0,0 +1,276 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +/* SPDX-FileCopyrightText: 2021-2022 Bartosz Golaszewski */ + +/** + * @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 +#include +#include + +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; + +/** + * @brief Vector of line values. + */ +using values = ::std::vector; + +/** + * @brief Represents a mapping of a line offset to line logical state. + */ +using value_mapping = ::std::pair; + +/** + * @brief Vector of offset->value mappings. Each mapping is defined as a pair + * of an unsigned and signed integers. + */ +using value_mappings = ::std::vector; + +/** + * @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 index 0000000..eab8eba --- /dev/null +++ b/bindings/cxx/gpiodcxx/misc.hpp @@ -0,0 +1,44 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +/* SPDX-FileCopyrightText: 2021 Bartosz Golaszewski */ + +/** + * @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 + +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 index 0000000..192bd91 --- /dev/null +++ b/bindings/cxx/gpiodcxx/request-builder.hpp @@ -0,0 +1,157 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +/* SPDX-FileCopyrightText: 2022 Bartosz Golaszewski */ + +/** + * @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 +#include + +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 _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 index 0000000..6ebbf99 --- /dev/null +++ b/bindings/cxx/gpiodcxx/request-config.hpp @@ -0,0 +1,114 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +/* SPDX-FileCopyrightText: 2021-2022 Bartosz Golaszewski */ + +/** + * @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 +#include +#include +#include + +#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 _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 index 0000000..fcb4d8d --- /dev/null +++ b/bindings/cxx/gpiodcxx/timestamp.hpp @@ -0,0 +1,123 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +/* SPDX-FileCopyrightText: 2022 Bartosz Golaszewski */ + +/** + * @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 +#include + +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 index 0000000..1f6d0d7 --- /dev/null +++ b/bindings/cxx/info-event.cpp @@ -0,0 +1,103 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later +// SPDX-FileCopyrightText: 2021-2022 Bartosz Golaszewski + +#include +#include + +#include "internal.hpp" + +namespace gpiod { + +namespace { + +const ::std::map 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 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 index 0000000..237d5d5 --- /dev/null +++ b/bindings/cxx/internal.cpp @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later +// SPDX-FileCopyrightText: 2021-2022 Bartosz Golaszewski + +#include +#include +#include + +#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 index 0000000..b64daf1 --- /dev/null +++ b/bindings/cxx/internal.hpp @@ -0,0 +1,234 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +/* SPDX-FileCopyrightText: 2021-2022 Bartosz Golaszewski */ + +#ifndef __LIBGPIOD_CXX_INTERNAL_HPP__ +#define __LIBGPIOD_CXX_INTERNAL_HPP__ + +#include +#include +#include +#include +#include +#include + +#include "gpiod.hpp" + +namespace gpiod { + +template +cxx_enum_type get_mapped_value(c_enum_type value, + const ::std::map& 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 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 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 copy(const ::std::shared_ptr& 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 copy(const ::std::shared_ptr& 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 copy(const ::std::shared_ptr& 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 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 index 0000000..731227c --- /dev/null +++ b/bindings/cxx/libgpiodcxx.pc.in @@ -0,0 +1,14 @@ +# SPDX-License-Identifier: GPL-2.0-or-later +# SPDX-FileCopyrightText: 2017-2021 Bartosz Golaszewski + +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 index 0000000..7e3dc8c --- /dev/null +++ b/bindings/cxx/line-config.cpp @@ -0,0 +1,167 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later +// SPDX-FileCopyrightText: 2021-2022 Bartosz Golaszewski + +#include +#include +#include +#include +#include +#include + +#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 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_config::get_line_settings() const +{ + ::std::size_t num_offsets = ::gpiod_line_config_get_num_configured_offsets( + this->_m_priv->config.get()); + ::std::map settings_map; + ::std::vector 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 index 0000000..2117f68 --- /dev/null +++ b/bindings/cxx/line-info.cpp @@ -0,0 +1,191 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later +// SPDX-FileCopyrightText: 2021-2022 Bartosz Golaszewski + +#include +#include +#include + +#include "internal.hpp" + +namespace gpiod { + +namespace { + +const ::std::map direction_mapping = { + { GPIOD_LINE_DIRECTION_INPUT, line::direction::INPUT }, + { GPIOD_LINE_DIRECTION_OUTPUT, line::direction::OUTPUT }, +}; + +const ::std::map 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 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 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 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 index 0000000..f6b0a66 --- /dev/null +++ b/bindings/cxx/line-request.cpp @@ -0,0 +1,241 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later +// SPDX-FileCopyrightText: 2021-2022 Bartosz Golaszewski + +#include +#include +#include + +#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 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(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 index 0000000..300858a --- /dev/null +++ b/bindings/cxx/line-settings.cpp @@ -0,0 +1,330 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later +// SPDX-FileCopyrightText: 2022 Bartosz Golaszewski + +#include +#include + +#include "internal.hpp" + +namespace gpiod { + +namespace { + +template +::std::map +make_reverse_maping(const ::std::map& mapping) +{ + ::std::map ret; + + for (const auto &item: mapping) + ret[item.second] = item.first; + + return ret; +} + +const ::std::map 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 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 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 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 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 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 +c_enum_type do_map_value(cxx_enum_type value, const ::std::map& mapping) +try { + return get_mapped_value(value, mapping); +} catch (const bad_mapping& ex) { + throw ::std::invalid_argument(ex.what()); +} + +template +void set_mapped_prop(::gpiod_line_settings* settings, cxx_enum_type value, + const ::std::map& 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 +cxx_enum_type get_mapped_prop(::gpiod_line_settings* settings, + const ::std::map& 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(this->_m_priv->settings.get(), + direction, direction_mapping); + + return *this; +} + +GPIOD_CXX_API line::direction line_settings::direction() const +{ + return get_mapped_prop( + this->_m_priv->settings.get(), + reverse_direction_mapping); +} + +GPIOD_CXX_API line_settings& line_settings::set_edge_detection(line::edge edge) +{ + set_mapped_prop(this->_m_priv->settings.get(), + edge, edge_mapping); + + return *this; +} + +GPIOD_CXX_API line::edge line_settings::edge_detection() const +{ + return get_mapped_prop( + this->_m_priv->settings.get(), + reverse_edge_mapping); +} + +GPIOD_CXX_API line_settings& line_settings::set_bias(line::bias bias) +{ + set_mapped_prop(this->_m_priv->settings.get(), + bias, bias_mapping); + + return *this; +} + +GPIOD_CXX_API line::bias line_settings::bias() const +{ + return get_mapped_prop(this->_m_priv->settings.get(), + reverse_bias_mapping); +} + +GPIOD_CXX_API line_settings& line_settings::set_drive(line::drive drive) +{ + set_mapped_prop(this->_m_priv->settings.get(), + drive, drive_mapping); + + return *this; +} + +GPIOD_CXX_API line::drive line_settings::drive() const +{ + return get_mapped_prop(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(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( + this->_m_priv->settings.get(), + reverse_clock_mapping); +} + +GPIOD_CXX_API line_settings& line_settings::set_output_value(line::value value) +{ + set_mapped_prop(this->_m_priv->settings.get(), + value, value_mapping); + + return *this; +} + +GPIOD_CXX_API line::value line_settings::output_value() const +{ + return get_mapped_prop( + 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 index 0000000..d6a92c1 --- /dev/null +++ b/bindings/cxx/line.cpp @@ -0,0 +1,132 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later +// SPDX-FileCopyrightText: 2021-2022 Bartosz Golaszewski + +#include +#include + +#include "internal.hpp" + +namespace gpiod { +namespace line { + +namespace { + +const ::std::map value_names = { + { line::value::INACTIVE, "INACTIVE" }, + { line::value::ACTIVE, "ACTIVE" }, +}; + +const ::std::map direction_names = { + { line::direction::AS_IS, "AS_IS" }, + { line::direction::INPUT, "INPUT" }, + { line::direction::OUTPUT, "OUTPUT" }, +}; + +const ::std::map 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 drive_names = { + { line::drive::PUSH_PULL, "PUSH_PULL" }, + { line::drive::OPEN_DRAIN, "OPEN_DRAIN" }, + { line::drive::OPEN_SOURCE, "OPEN_SOURCE" }, +}; + +const ::std::map edge_names = { + { line::edge::NONE, "NONE" }, + { line::edge::RISING, "RISING_EDGE" }, + { line::edge::FALLING, "FALLING_EDGE" }, + { line::edge::BOTH, "BOTH_EDGES" }, +}; + +const ::std::map 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 +::std::ostream& insert_vector(::std::ostream& out, + const ::std::string& name, const ::std::vector& vec) +{ + out << name << "("; + ::std::copy(vec.begin(), ::std::prev(vec.end()), + ::std::ostream_iterator(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 index 0000000..eba0b46 --- /dev/null +++ b/bindings/cxx/misc.cpp @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later +// SPDX-FileCopyrightText: 2021-2022 Bartosz Golaszewski + +#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 index 0000000..87ee2fe --- /dev/null +++ b/bindings/cxx/request-builder.cpp @@ -0,0 +1,142 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later +// SPDX-FileCopyrightText: 2022 Bartosz Golaszewski + +#include +#include + +#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 index 0000000..b44b8b6 --- /dev/null +++ b/bindings/cxx/request-config.cpp @@ -0,0 +1,103 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later +// SPDX-FileCopyrightText: 2021 Bartosz Golaszewski + +#include +#include + +#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 index 0000000..7990193 --- /dev/null +++ b/bindings/cxx/tests/.gitignore @@ -0,0 +1,4 @@ +# SPDX-License-Identifier: GPL-2.0-or-later +# SPDX-FileCopyrightText: 2017-2021 Bartosz Golaszewski + +gpiod-cxx-test diff --git a/bindings/cxx/tests/Makefile.am b/bindings/cxx/tests/Makefile.am new file mode 100644 index 0000000..fbf80a1 --- /dev/null +++ b/bindings/cxx/tests/Makefile.am @@ -0,0 +1,30 @@ +# SPDX-License-Identifier: GPL-2.0-or-later +# SPDX-FileCopyrightText: 2017-2021 Bartosz Golaszewski + +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 index 0000000..e10fb5d --- /dev/null +++ b/bindings/cxx/tests/check-kernel.cpp @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +// SPDX-FileCopyrightText: 2017-2022 Bartosz Golaszewski + +#include +#include +#include +#include + +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 index 0000000..11bf8e5 --- /dev/null +++ b/bindings/cxx/tests/gpiod-cxx-test-main.cpp @@ -0,0 +1,5 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +// SPDX-FileCopyrightText: 2017-2021 Bartosz Golaszewski + +#define CATCH_CONFIG_MAIN +#include diff --git a/bindings/cxx/tests/gpiosim.cpp b/bindings/cxx/tests/gpiosim.cpp new file mode 100644 index 0000000..4bda5a2 --- /dev/null +++ b/bindings/cxx/tests/gpiosim.cpp @@ -0,0 +1,277 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +/* SPDX-FileCopyrightText: 2022 Bartosz Golaszewski */ + +#include +#include +#include + +#include "gpiosim.h" +#include "gpiosim.hpp" + +namespace gpiosim { + +namespace { + +const ::std::map pull_mapping = { + { chip::pull::PULL_UP, GPIOSIM_PULL_UP }, + { chip::pull::PULL_DOWN, GPIOSIM_PULL_DOWN }, +}; + +const ::std::map 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 value_mapping = { + { GPIOSIM_VALUE_INACTIVE, chip::value::INACTIVE }, + { GPIOSIM_VALUE_ACTIVE, chip::value::ACTIVE }, +}; + +template 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 line_names; + ::std::map> 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 index 0000000..8151af6 --- /dev/null +++ b/bindings/cxx/tests/gpiosim.hpp @@ -0,0 +1,86 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +/* SPDX-FileCopyrightText: 2022 Bartosz Golaszewski */ + +#ifndef __GPIOD_CXX_GPIOSIM_HPP__ +#define __GPIOD_CXX_GPIOSIM_HPP__ + +#include +#include + +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 _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 _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 index 0000000..eb2c3db --- /dev/null +++ b/bindings/cxx/tests/helpers.cpp @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later +// SPDX-FileCopyrightText: 2021-2022 Bartosz Golaszewski + +#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 index 0000000..62d9827 --- /dev/null +++ b/bindings/cxx/tests/helpers.hpp @@ -0,0 +1,62 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +/* SPDX-FileCopyrightText: 2021-2022 Bartosz Golaszewski */ + +#ifndef __GPIOD_CXX_TEST_HELPERS_HPP__ +#define __GPIOD_CXX_TEST_HELPERS_HPP__ + +#include +#include +#include +#include +#include + +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 stringify_matcher : public Catch::MatcherBase +{ +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 index 0000000..717c387 --- /dev/null +++ b/bindings/cxx/tests/tests-chip-info.cpp @@ -0,0 +1,117 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +// SPDX-FileCopyrightText: 2021-2022 Bartosz Golaszewski + +#include +#include +#include + +#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 index 0000000..c5ec19b --- /dev/null +++ b/bindings/cxx/tests/tests-chip.cpp @@ -0,0 +1,182 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +// SPDX-FileCopyrightText: 2021-2022 Bartosz Golaszewski + +#include +#include +#include +#include +#include + +#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 index 0000000..19a6ab3 --- /dev/null +++ b/bindings/cxx/tests/tests-edge-event.cpp @@ -0,0 +1,422 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +// SPDX-FileCopyrightText: 2022 Bartosz Golaszewski + +#include +#include +#include +#include +#include +#include + +#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 index 0000000..21c0ef0 --- /dev/null +++ b/bindings/cxx/tests/tests-info-event.cpp @@ -0,0 +1,225 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +// SPDX-FileCopyrightText: 2022 Bartosz Golaszewski + +#include +#include +#include +#include +#include +#include +#include + +#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 index 0000000..5e439a1 --- /dev/null +++ b/bindings/cxx/tests/tests-line-config.cpp @@ -0,0 +1,155 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +// SPDX-FileCopyrightText: 2022 Bartosz Golaszewski + +#include +#include + +#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 index 0000000..21211f2 --- /dev/null +++ b/bindings/cxx/tests/tests-line-info.cpp @@ -0,0 +1,154 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +// SPDX-FileCopyrightText: 2022 Bartosz Golaszewski + +#include +#include +#include + +#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 index 0000000..6e29532 --- /dev/null +++ b/bindings/cxx/tests/tests-line-request.cpp @@ -0,0 +1,501 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +// SPDX-FileCopyrightText: 2022 Bartosz Golaszewski + +#include +#include +#include +#include +#include + +#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 +{ +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 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 index 0000000..dc821bb --- /dev/null +++ b/bindings/cxx/tests/tests-line-settings.cpp @@ -0,0 +1,188 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +// SPDX-FileCopyrightText: 2022 Bartosz Golaszewski + +#include +#include + +#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(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(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(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(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(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(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 index 0000000..319012a --- /dev/null +++ b/bindings/cxx/tests/tests-line.cpp @@ -0,0 +1,139 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +// SPDX-FileCopyrightText: 2021-2022 Bartosz Golaszewski + +#include +#include + +#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("4")); + } + + SECTION("value") + { + auto active = value::ACTIVE; + auto inactive = value::INACTIVE; + + REQUIRE_THAT(active, stringify_matcher("ACTIVE")); + REQUIRE_THAT(inactive, stringify_matcher("INACTIVE")); + } + + SECTION("direction") + { + auto input = direction::INPUT; + auto output = direction::OUTPUT; + auto as_is = direction::AS_IS; + + REQUIRE_THAT(input, stringify_matcher("INPUT")); + REQUIRE_THAT(output, stringify_matcher("OUTPUT")); + REQUIRE_THAT(as_is, stringify_matcher("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("RISING_EDGE")); + REQUIRE_THAT(falling, stringify_matcher("FALLING_EDGE")); + REQUIRE_THAT(both, stringify_matcher("BOTH_EDGES")); + REQUIRE_THAT(none, stringify_matcher("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("PULL_UP")); + REQUIRE_THAT(pull_down, stringify_matcher("PULL_DOWN")); + REQUIRE_THAT(disabled, stringify_matcher("DISABLED")); + REQUIRE_THAT(as_is, stringify_matcher("AS_IS")); + REQUIRE_THAT(unknown, stringify_matcher("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("PUSH_PULL")); + REQUIRE_THAT(open_drain, stringify_matcher("OPEN_DRAIN")); + REQUIRE_THAT(open_source, stringify_matcher("OPEN_SOURCE")); + } + + SECTION("clock") + { + auto monotonic = clock_type::MONOTONIC; + auto realtime = clock_type::REALTIME; + auto hte = clock_type::HTE; + + REQUIRE_THAT(monotonic, stringify_matcher("MONOTONIC")); + REQUIRE_THAT(realtime, stringify_matcher("REALTIME")); + REQUIRE_THAT(hte, stringify_matcher("HTE")); + } + + SECTION("offsets") + { + offsets offs = { 2, 5, 3, 9, 8, 7 }; + + REQUIRE_THAT(offs, stringify_matcher("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("gpiod::values(ACTIVE, INACTIVE, ACTIVE, ACTIVE, INACTIVE)")); + } + + SECTION("value_mapping") + { + value_mapping val = { 4, value::ACTIVE }; + + REQUIRE_THAT(val, stringify_matcher("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( + "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 index 0000000..f06dc39 --- /dev/null +++ b/bindings/cxx/tests/tests-misc.cpp @@ -0,0 +1,78 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +// SPDX-FileCopyrightText: 2021-2022 Bartosz Golaszewski + +#include +#include +#include +#include +#include +#include + +#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 index 0000000..66eb748 --- /dev/null +++ b/bindings/cxx/tests/tests-request-config.cpp @@ -0,0 +1,83 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +// SPDX-FileCopyrightText: 2021-2022 Bartosz Golaszewski + +#include +#include +#include +#include +#include + +#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 index 0000000..fce31f5 --- /dev/null +++ b/bindings/python/.gitignore @@ -0,0 +1,8 @@ +# SPDX-License-Identifier: GPL-2.0-or-later +# SPDX-FileCopyrightText: 2017-2022 Bartosz Golaszewski + +build/ +__pycache__/ +dist/ +*.egg-info/ +*.so diff --git a/bindings/python/MANIFEST.in b/bindings/python/MANIFEST.in new file mode 100644 index 0000000..efdfd18 --- /dev/null +++ b/bindings/python/MANIFEST.in @@ -0,0 +1,19 @@ +# SPDX-License-Identifier: CC0-1.0 +# SPDX-FileCopyrightText: 2023 Bartosz Golaszewski + +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 index 0000000..61d82c5 --- /dev/null +++ b/bindings/python/Makefile.am @@ -0,0 +1,36 @@ +# SPDX-License-Identifier: GPL-2.0-or-later +# SPDX-FileCopyrightText: 2022 Bartosz Golaszewski + +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 index 0000000..abb69da --- /dev/null +++ b/bindings/python/README.md @@ -0,0 +1,104 @@ +# SPDX-License-Identifier: CC-BY-SA-4.0 +# SPDX-FileCopyrightText: 2023 Phil Howard + +# 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 index 0000000..8852312 --- /dev/null +++ b/bindings/python/examples/Makefile.am @@ -0,0 +1,17 @@ +# SPDX-License-Identifier: GPL-2.0-or-later +# SPDX-FileCopyrightText: 2022 Bartosz Golaszewski + +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 index 0000000..1d6a184 --- /dev/null +++ b/bindings/python/examples/async_watch_line_value.py @@ -0,0 +1,54 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: GPL-2.0-or-later +# SPDX-FileCopyrightText: 2023 Kent Gibson + +"""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 index 0000000..ac798a9 --- /dev/null +++ b/bindings/python/examples/find_line_by_name.py @@ -0,0 +1,37 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: GPL-2.0-or-later +# SPDX-FileCopyrightText: 2023 Kent Gibson + +"""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 index 0000000..7dc76fb --- /dev/null +++ b/bindings/python/examples/get_chip_info.py @@ -0,0 +1,20 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: GPL-2.0-or-later +# SPDX-FileCopyrightText: 2023 Kent Gibson + +"""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 index 0000000..cd4ebcc --- /dev/null +++ b/bindings/python/examples/get_line_info.py @@ -0,0 +1,29 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: GPL-2.0-or-later +# SPDX-FileCopyrightText: 2023 Kent Gibson + +"""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 index 0000000..f3ca13b --- /dev/null +++ b/bindings/python/examples/get_line_value.py @@ -0,0 +1,26 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: GPL-2.0-or-later +# SPDX-FileCopyrightText: 2023 Kent Gibson + +"""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 index 0000000..46cf0b2 --- /dev/null +++ b/bindings/python/examples/get_multiple_line_values.py @@ -0,0 +1,29 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: GPL-2.0-or-later +# SPDX-FileCopyrightText: 2023 Kent Gibson + +"""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 index 0000000..0f2e358 --- /dev/null +++ b/bindings/python/examples/reconfigure_input_to_output.py @@ -0,0 +1,39 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: GPL-2.0-or-later +# SPDX-FileCopyrightText: 2023 Kent Gibson + +"""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 index 0000000..e0de8fb --- /dev/null +++ b/bindings/python/examples/toggle_line_value.py @@ -0,0 +1,43 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: GPL-2.0-or-later +# SPDX-FileCopyrightText: 2023 Kent Gibson + +"""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 index 0000000..12b968d --- /dev/null +++ b/bindings/python/examples/toggle_multiple_line_values.py @@ -0,0 +1,47 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: GPL-2.0-or-later +# SPDX-FileCopyrightText: 2023 Kent Gibson + +"""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 index 0000000..e49a669 --- /dev/null +++ b/bindings/python/examples/watch_line_info.py @@ -0,0 +1,23 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: GPL-2.0-or-later +# SPDX-FileCopyrightText: 2023 Kent Gibson + +"""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 index 0000000..7b1c6b0 --- /dev/null +++ b/bindings/python/examples/watch_line_rising.py @@ -0,0 +1,32 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: GPL-2.0-or-later +# SPDX-FileCopyrightText: 2023 Kent Gibson + +"""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 index 0000000..171a67c --- /dev/null +++ b/bindings/python/examples/watch_line_value.py @@ -0,0 +1,49 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: GPL-2.0-or-later +# SPDX-FileCopyrightText: 2023 Kent Gibson + +"""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 index 0000000..5906b7d --- /dev/null +++ b/bindings/python/examples/watch_multiple_line_values.py @@ -0,0 +1,42 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: GPL-2.0-or-later +# SPDX-FileCopyrightText: 2023 Kent Gibson + +"""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 index 0000000..daf7bae --- /dev/null +++ b/bindings/python/gpiod/Makefile.am @@ -0,0 +1,18 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +# SPDX-FileCopyrightText: 2022 Bartosz Golaszewski + +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 index 0000000..9cbb8df --- /dev/null +++ b/bindings/python/gpiod/__init__.py @@ -0,0 +1,54 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +# SPDX-FileCopyrightText: 2022 Bartosz Golaszewski + +""" +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 index 0000000..b3d8e61 --- /dev/null +++ b/bindings/python/gpiod/chip.py @@ -0,0 +1,363 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +# SPDX-FileCopyrightText: 2022 Bartosz Golaszewski + +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 "" + + return 'Chip("{}")'.format(self.path) + + def __str__(self) -> str: + """ + Return a user-friendly, human-readable description of this chip. + """ + if not self._chip: + return "" + + return ''.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 index 0000000..92b5e6f --- /dev/null +++ b/bindings/python/gpiod/chip_info.py @@ -0,0 +1,23 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +# SPDX-FileCopyrightText: 2022 Bartosz Golaszewski + + +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 ''.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 index 0000000..bf258c1 --- /dev/null +++ b/bindings/python/gpiod/edge_event.py @@ -0,0 +1,48 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +# SPDX-FileCopyrightText: 2022 Bartosz Golaszewski + +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 "".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 index 0000000..f9a83c2 --- /dev/null +++ b/bindings/python/gpiod/exception.py @@ -0,0 +1,22 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +# SPDX-FileCopyrightText: 2022 Bartosz Golaszewski + +__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 index 0000000..9c81b17 --- /dev/null +++ b/bindings/python/gpiod/ext/Makefile.am @@ -0,0 +1,11 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +# SPDX-FileCopyrightText: 2022 Bartosz Golaszewski + +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 index 0000000..e8eaad8 --- /dev/null +++ b/bindings/python/gpiod/ext/chip.c @@ -0,0 +1,341 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later +// SPDX-FileCopyrightText: 2022 Bartosz Golaszewski + +#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 index 0000000..07fca8c --- /dev/null +++ b/bindings/python/gpiod/ext/common.c @@ -0,0 +1,92 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later +// SPDX-FileCopyrightText: 2022 Bartosz Golaszewski + +#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 index 0000000..7d223c0 --- /dev/null +++ b/bindings/python/gpiod/ext/internal.h @@ -0,0 +1,19 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +/* SPDX-FileCopyrightText: 2022 Bartosz Golaszewski */ + +#ifndef __LIBGPIOD_PYTHON_MODULE_H__ +#define __LIBGPIOD_PYTHON_MODULE_H__ + +#include +#include + +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 index 0000000..0bba112 --- /dev/null +++ b/bindings/python/gpiod/ext/line-config.c @@ -0,0 +1,197 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later +// SPDX-FileCopyrightText: 2022 Bartosz Golaszewski + +#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 index 0000000..650235e --- /dev/null +++ b/bindings/python/gpiod/ext/line-settings.c @@ -0,0 +1,126 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later +// SPDX-FileCopyrightText: 2022 Bartosz Golaszewski + +#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 index 0000000..b456190 --- /dev/null +++ b/bindings/python/gpiod/ext/module.c @@ -0,0 +1,206 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later +// SPDX-FileCopyrightText: 2022 Bartosz Golaszewski + +#include +#include + +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 index 0000000..5db69fe --- /dev/null +++ b/bindings/python/gpiod/ext/request.c @@ -0,0 +1,413 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later +// SPDX-FileCopyrightText: 2022 Bartosz Golaszewski + +#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 index 0000000..481eae6 --- /dev/null +++ b/bindings/python/gpiod/info_event.py @@ -0,0 +1,35 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +# SPDX-FileCopyrightText: 2022 Bartosz Golaszewski + +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 "".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 index 0000000..2dddb65 --- /dev/null +++ b/bindings/python/gpiod/internal.py @@ -0,0 +1,18 @@ +# SPDX-License-Identifier: GPL-2.0-or-later +# SPDX-FileCopyrightText: 2022 Bartosz Golaszewski + +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 index 0000000..1cc512f --- /dev/null +++ b/bindings/python/gpiod/line.py @@ -0,0 +1,58 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +# SPDX-FileCopyrightText: 2022 Bartosz Golaszewski + + +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 index 0000000..c196a6a --- /dev/null +++ b/bindings/python/gpiod/line_info.py @@ -0,0 +1,75 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +# SPDX-FileCopyrightText: 2022 Bartosz Golaszewski + +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 ''.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 index 0000000..cde298f --- /dev/null +++ b/bindings/python/gpiod/line_request.py @@ -0,0 +1,258 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +# SPDX-FileCopyrightText: 2022 Bartosz Golaszewski + +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 "" + + return ''.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 index 0000000..458fd81 --- /dev/null +++ b/bindings/python/gpiod/line_settings.py @@ -0,0 +1,64 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +# SPDX-FileCopyrightText: 2022 Bartosz Golaszewski + +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: + 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 "".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 index 0000000..2e182a3 --- /dev/null +++ b/bindings/python/gpiod/version.py @@ -0,0 +1,5 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +# SPDX-FileCopyrightText: 2022 Linaro Ltd. +# SPDX-FileCopyrightText: 2022 Bartosz Golaszewski + +__version__ = "2.0.1" diff --git a/bindings/python/pyproject.toml b/bindings/python/pyproject.toml new file mode 100644 index 0000000..f6bf43c --- /dev/null +++ b/bindings/python/pyproject.toml @@ -0,0 +1,5 @@ +# SPDX-License-Identifier: GPL-2.0-or-later +# SPDX-FileCopyrightText: 2023 Phil Howard + +[build-system] +requires = ["setuptools", "wheel", "packaging"] diff --git a/bindings/python/setup.py b/bindings/python/setup.py new file mode 100644 index 0000000..904b5f3 --- /dev/null +++ b/bindings/python/setup.py @@ -0,0 +1,264 @@ +# SPDX-License-Identifier: GPL-2.0-or-later +# SPDX-FileCopyrightText: 2022 Bartosz Golaszewski + +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 index 0000000..c89241e --- /dev/null +++ b/bindings/python/tests/Makefile.am @@ -0,0 +1,17 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +# SPDX-FileCopyrightText: 2022 Bartosz Golaszewski + +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 index 0000000..02f4e8d --- /dev/null +++ b/bindings/python/tests/__init__.py @@ -0,0 +1,17 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +# SPDX-FileCopyrightText: 2022 Bartosz Golaszewski + +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 index 0000000..cc39c29 --- /dev/null +++ b/bindings/python/tests/__main__.py @@ -0,0 +1,20 @@ +#!/usr/bin/python3 +# SPDX-License-Identifier: GPL-2.0-or-later +# SPDX-FileCopyrightText: 2022 Bartosz Golaszewski + +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 index 0000000..7004f3a --- /dev/null +++ b/bindings/python/tests/gpiosim/Makefile.am @@ -0,0 +1,7 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +# SPDX-FileCopyrightText: 2022 Bartosz Golaszewski + +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 index 0000000..f65e413 --- /dev/null +++ b/bindings/python/tests/gpiosim/__init__.py @@ -0,0 +1,4 @@ +# SPDX-License-Identifier: GPL-2.0-or-later +# SPDX-FileCopyrightText: 2022 Bartosz Golaszewski + +from .chip import Chip diff --git a/bindings/python/tests/gpiosim/chip.py b/bindings/python/tests/gpiosim/chip.py new file mode 100644 index 0000000..6af883e --- /dev/null +++ b/bindings/python/tests/gpiosim/chip.py @@ -0,0 +1,65 @@ +# SPDX-License-Identifier: GPL-2.0-or-later +# SPDX-FileCopyrightText: 2022 Bartosz Golaszewski + +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 index 0000000..272e6f7 --- /dev/null +++ b/bindings/python/tests/gpiosim/ext.c @@ -0,0 +1,345 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later +// SPDX-FileCopyrightText: 2022 Bartosz Golaszewski + +#include +#include + +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 index 0000000..f9a15e8 --- /dev/null +++ b/bindings/python/tests/helpers.py @@ -0,0 +1,16 @@ +# SPDX-License-Identifier: GPL-2.0-or-later +# SPDX-FileCopyrightText: 2022 Bartosz Golaszewski + +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 index 0000000..c4a8fd5 --- /dev/null +++ b/bindings/python/tests/procname/Makefile.am @@ -0,0 +1,6 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +# SPDX-FileCopyrightText: 2022 Bartosz Golaszewski + +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 index 0000000..af6abdd --- /dev/null +++ b/bindings/python/tests/procname/__init__.py @@ -0,0 +1,4 @@ +# SPDX-License-Identifier: GPL-2.0-or-later +# SPDX-FileCopyrightText: 2022 Bartosz Golaszewski + +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 index 0000000..bf7d2fe --- /dev/null +++ b/bindings/python/tests/procname/ext.c @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later +// SPDX-FileCopyrightText: 2022 Bartosz Golaszewski + +#include +#include + +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 index 0000000..8db4cdb --- /dev/null +++ b/bindings/python/tests/tests_chip.py @@ -0,0 +1,231 @@ +# SPDX-License-Identifier: GPL-2.0-or-later +# SPDX-FileCopyrightText: 2022 Bartosz Golaszewski + +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), + '>'.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), "") + + def test_str_closed(self): + self.chip.close() + self.assertEqual(str(self.chip), "") diff --git a/bindings/python/tests/tests_chip_info.py b/bindings/python/tests/tests_chip_info.py new file mode 100644 index 0000000..d392ec3 --- /dev/null +++ b/bindings/python/tests/tests_chip_info.py @@ -0,0 +1,52 @@ +# SPDX-License-Identifier: GPL-2.0-or-later +# SPDX-FileCopyrightText: 2022 Bartosz Golaszewski + +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), + ''.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 index 0000000..430b27d --- /dev/null +++ b/bindings/python/tests/tests_edge_event.py @@ -0,0 +1,212 @@ +# SPDX-License-Identifier: GPL-2.0-or-later +# SPDX-FileCopyrightText: 2022 Bartosz Golaszewski + +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), + "", + ) diff --git a/bindings/python/tests/tests_info_event.py b/bindings/python/tests/tests_info_event.py new file mode 100644 index 0000000..6bb09d5 --- /dev/null +++ b/bindings/python/tests/tests_info_event.py @@ -0,0 +1,189 @@ +# SPDX-License-Identifier: GPL-2.0-or-later +# SPDX-FileCopyrightText: 2022 Bartosz Golaszewski + +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), + '>', + ) diff --git a/bindings/python/tests/tests_line_info.py b/bindings/python/tests/tests_line_info.py new file mode 100644 index 0000000..2779e7a --- /dev/null +++ b/bindings/python/tests/tests_line_info.py @@ -0,0 +1,101 @@ +# SPDX-License-Identifier: GPL-2.0-or-later +# SPDX-FileCopyrightText: 2022 Bartosz Golaszewski + +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), + '', + ) diff --git a/bindings/python/tests/tests_line_request.py b/bindings/python/tests/tests_line_request.py new file mode 100644 index 0000000..f99b93d --- /dev/null +++ b/bindings/python/tests/tests_line_request.py @@ -0,0 +1,544 @@ +# SPDX-License-Identifier: GPL-2.0-or-later +# SPDX-FileCopyrightText: 2022 Bartosz Golaszewski + +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), + ''.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), "") diff --git a/bindings/python/tests/tests_line_settings.py b/bindings/python/tests/tests_line_settings.py new file mode 100644 index 0000000..36dda6d --- /dev/null +++ b/bindings/python/tests/tests_line_settings.py @@ -0,0 +1,79 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +# SPDX-FileCopyrightText: 2022 Bartosz Golaszewski + +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), + "", + ) diff --git a/bindings/python/tests/tests_module.py b/bindings/python/tests/tests_module.py new file mode 100644 index 0000000..c6f07a6 --- /dev/null +++ b/bindings/python/tests/tests_module.py @@ -0,0 +1,60 @@ +# SPDX-License-Identifier: GPL-2.0-or-later +# SPDX-FileCopyrightText: 2022 Bartosz Golaszewski + +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 index 0000000..a8bda09 --- /dev/null +++ b/bindings/rust/.gitignore @@ -0,0 +1,6 @@ +# SPDX-License-Identifier: CC0-1.0 +# SPDX-FileCopyrightText: 2022 Linaro Ltd. +# SPDX-FileCopyrightText: 2022 Viresh Kumar + +target +Cargo.lock diff --git a/bindings/rust/Cargo.toml b/bindings/rust/Cargo.toml new file mode 100644 index 0000000..e385027 --- /dev/null +++ b/bindings/rust/Cargo.toml @@ -0,0 +1,13 @@ +# SPDX-License-Identifier: CC0-1.0 +# SPDX-FileCopyrightText: 2022 Linaro Ltd. +# SPDX-FileCopyrightText: 2022 Viresh Kumar + +[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 index 0000000..e89c393 --- /dev/null +++ b/bindings/rust/Makefile.am @@ -0,0 +1,6 @@ +# SPDX-License-Identifier: GPL-2.0-or-later +# SPDX-FileCopyrightText: 2022 Linaro Ltd. +# SPDX-FileCopyrightText: 2022 Viresh Kumar + +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 index 0000000..1f44a31 --- /dev/null +++ b/bindings/rust/gpiosim-sys/Cargo.toml @@ -0,0 +1,23 @@ +# SPDX-License-Identifier: CC0-1.0 +# SPDX-FileCopyrightText: 2022 Linaro Ltd. +# SPDX-FileCopyrightText: 2022 Viresh Kumar + +[package] +name = "gpiosim-sys" +version = "0.1.0" +authors = ["Viresh Kumar "] +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 index 0000000..3107223 --- /dev/null +++ b/bindings/rust/gpiosim-sys/Makefile.am @@ -0,0 +1,6 @@ +# SPDX-License-Identifier: GPL-2.0-or-later +# SPDX-FileCopyrightText: 2022 Linaro Ltd. +# SPDX-FileCopyrightText: 2022 Bartosz Golaszewski + +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 index 0000000..b13f09a --- /dev/null +++ b/bindings/rust/gpiosim-sys/README.md @@ -0,0 +1,16 @@ + + +# 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 index 0000000..c31fccb --- /dev/null +++ b/bindings/rust/gpiosim-sys/build.rs @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: Apache-2.0 OR BSD-3-Clause +// SPDX-FileCopyrightText: 2022 Linaro Ltd. +// SPDX-FileCopyrightText: 2022 Viresh Kumar + +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 index 0000000..e88f477 --- /dev/null +++ b/bindings/rust/gpiosim-sys/src/Makefile.am @@ -0,0 +1,5 @@ +# SPDX-License-Identifier: GPL-2.0-or-later +# SPDX-FileCopyrightText: 2022 Linaro Ltd. +# SPDX-FileCopyrightText: 2022 Bartosz Golaszewski + +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 index 0000000..bf9ae32 --- /dev/null +++ b/bindings/rust/gpiosim-sys/src/lib.rs @@ -0,0 +1,91 @@ +// SPDX-License-Identifier: Apache-2.0 OR BSD-3-Clause +// SPDX-FileCopyrightText: 2022 Linaro Ltd. +// SPDX-FileCopyrightText: 2022 Viresh Kumar + +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 { + 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 index 0000000..71b9453 --- /dev/null +++ b/bindings/rust/gpiosim-sys/src/sim.rs @@ -0,0 +1,330 @@ +// SPDX-License-Identifier: Apache-2.0 OR BSD-3-Clause +// SPDX-FileCopyrightText: 2022 Linaro Ltd. +// SPDX-FileCopyrightText: 2022 Viresh Kumar + +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 { + // 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 { + // 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 { + // 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 { + // 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 { + // 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, label: Option<&str>, enable: bool) -> Result { + 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 { + 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 index 0000000..b4d26e9 --- /dev/null +++ b/bindings/rust/libgpiod-sys/Cargo.toml @@ -0,0 +1,28 @@ +# SPDX-License-Identifier: CC0-1.0 +# SPDX-FileCopyrightText: 2022 Linaro Ltd. +# SPDX-FileCopyrightText: 2022 Viresh Kumar + +[package] +name = "libgpiod-sys" +version = "0.1.0" +authors = ["Viresh Kumar "] +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 index 0000000..667f3de --- /dev/null +++ b/bindings/rust/libgpiod-sys/Makefile.am @@ -0,0 +1,6 @@ +# SPDX-License-Identifier: GPL-2.0-or-later +# SPDX-FileCopyrightText: 2022 Linaro Ltd. +# SPDX-FileCopyrightText: 2022 Bartosz Golaszewski + +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 index 0000000..05acd9e --- /dev/null +++ b/bindings/rust/libgpiod-sys/README.md @@ -0,0 +1,36 @@ + + +# 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="/lib/.libs/" + export SYSTEM_DEPS_LIBGPIOD_LIB=gpiod + export SYSTEM_DEPS_LIBGPIOD_INCLUDE="/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 index 0000000..9e6a93c --- /dev/null +++ b/bindings/rust/libgpiod-sys/build.rs @@ -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 +// SPDX-FileCopyrightText: 2023 Erik Schilling + +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 index 0000000..0ef728b --- /dev/null +++ b/bindings/rust/libgpiod-sys/src/Makefile.am @@ -0,0 +1,5 @@ +# SPDX-License-Identifier: GPL-2.0-or-later +# SPDX-FileCopyrightText: 2022 Linaro Ltd. +# SPDX-FileCopyrightText: 2022 Bartosz Golaszewski + +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 index 0000000..06f1a50 --- /dev/null +++ b/bindings/rust/libgpiod-sys/src/lib.rs @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: Apache-2.0 OR BSD-3-Clause +// SPDX-FileCopyrightText: 2022 Linaro Ltd. +// SPDX-FileCopyrightText: 2022 Viresh Kumar + +#[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 index 0000000..7a350a4 --- /dev/null +++ b/bindings/rust/libgpiod-sys/wrapper.h @@ -0,0 +1,5 @@ +/* SPDX-License-Identifier: Apache-2.0 OR BSD-3-Clause */ +/* SPDX-FileCopyrightText: 2023 Linaro Ltd. */ +/* SPDX-FileCopyrightText: 2023 Erik Schilling */ + +#include diff --git a/bindings/rust/libgpiod/Cargo.toml b/bindings/rust/libgpiod/Cargo.toml new file mode 100644 index 0000000..3ce05df --- /dev/null +++ b/bindings/rust/libgpiod/Cargo.toml @@ -0,0 +1,32 @@ +# SPDX-License-Identifier: CC0-1.0 +# SPDX-FileCopyrightText: 2022 Linaro Ltd. +# SPDX-FileCopyrightText: 2022 Viresh Kumar + +[package] +name = "libgpiod" +version = "0.2.1" +authors = ["Viresh Kumar "] +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 index 0000000..619e36c --- /dev/null +++ b/bindings/rust/libgpiod/Makefile.am @@ -0,0 +1,28 @@ +# SPDX-License-Identifier: GPL-2.0-or-later +# SPDX-FileCopyrightText: 2022 Linaro Ltd. +# SPDX-FileCopyrightText: 2022 Bartosz Golaszewski + +# 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 index 0000000..c86b06e --- /dev/null +++ b/bindings/rust/libgpiod/README.md @@ -0,0 +1,38 @@ + + +# 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 index 0000000..48b182c --- /dev/null +++ b/bindings/rust/libgpiod/examples/Makefile.am @@ -0,0 +1,18 @@ +# SPDX-License-Identifier: GPL-2.0-or-later +# SPDX-FileCopyrightText: 2022 Linaro Ltd. +# SPDX-FileCopyrightText: 2022 Bartosz Golaszewski + +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 index 0000000..8dbb496 --- /dev/null +++ b/bindings/rust/libgpiod/examples/buffered_event_lifetimes.rs @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: Apache-2.0 OR BSD-3-Clause +// SPDX-FileCopyrightText: 2022 Linaro Ltd. +// SPDX-FileCopyrightText: 2022 Viresh Kumar +// +// 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 index 0000000..f7b9919 --- /dev/null +++ b/bindings/rust/libgpiod/examples/find_line_by_name.rs @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: Apache-2.0 OR BSD-3-Clause +// SPDX-FileCopyrightText: 2023 Kent Gibson +// +// 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 index 0000000..cc23c86 --- /dev/null +++ b/bindings/rust/libgpiod/examples/get_chip_info.rs @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: Apache-2.0 OR BSD-3-Clause +// SPDX-FileCopyrightText: 2023 Kent Gibson +// +// 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 index 0000000..086db90 --- /dev/null +++ b/bindings/rust/libgpiod/examples/get_line_info.rs @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: Apache-2.0 OR BSD-3-Clause +// SPDX-FileCopyrightText: 2023 Kent Gibson +// +// 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 index 0000000..39141e2 --- /dev/null +++ b/bindings/rust/libgpiod/examples/get_line_value.rs @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: Apache-2.0 OR BSD-3-Clause +// SPDX-FileCopyrightText: 2023 Kent Gibson +// +// 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 index 0000000..a1be5a6 --- /dev/null +++ b/bindings/rust/libgpiod/examples/get_multiple_line_values.rs @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: Apache-2.0 OR BSD-3-Clause +// SPDX-FileCopyrightText: 2023 Kent Gibson +// +// 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 index 0000000..fb5402b --- /dev/null +++ b/bindings/rust/libgpiod/examples/reconfigure_input_to_output.rs @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: Apache-2.0 OR BSD-3-Clause +// SPDX-FileCopyrightText: 2023 Kent Gibson +// +// 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 index 0000000..6d5f697 --- /dev/null +++ b/bindings/rust/libgpiod/examples/toggle_line_value.rs @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: Apache-2.0 OR BSD-3-Clause +// SPDX-FileCopyrightText: 2023 Kent Gibson +// +// 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 index 0000000..b7e6915 --- /dev/null +++ b/bindings/rust/libgpiod/examples/toggle_multiple_line_values.rs @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: Apache-2.0 OR BSD-3-Clause +// SPDX-FileCopyrightText: 2023 Kent Gibson +// +// 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 index 0000000..e84ce13 --- /dev/null +++ b/bindings/rust/libgpiod/examples/watch_line_info.rs @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: Apache-2.0 OR BSD-3-Clause +// SPDX-FileCopyrightText: 2023 Kent Gibson +// +// 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 index 0000000..81a2407 --- /dev/null +++ b/bindings/rust/libgpiod/examples/watch_line_rising.rs @@ -0,0 +1,44 @@ +// SPDX-License-Identifier: Apache-2.0 OR BSD-3-Clause +// SPDX-FileCopyrightText: 2023 Kent Gibson +// +// 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 index 0000000..3bf40af --- /dev/null +++ b/bindings/rust/libgpiod/examples/watch_line_value.rs @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: Apache-2.0 OR BSD-3-Clause +// SPDX-FileCopyrightText: 2023 Kent Gibson +// +// 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 index 0000000..3fc88ba --- /dev/null +++ b/bindings/rust/libgpiod/examples/watch_multiple_line_values.rs @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: Apache-2.0 OR BSD-3-Clause +// SPDX-FileCopyrightText: 2023 Kent Gibson +// +// 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 index 0000000..5892600 --- /dev/null +++ b/bindings/rust/libgpiod/src/Makefile.am @@ -0,0 +1,15 @@ +# SPDX-License-Identifier: GPL-2.0-or-later +# SPDX-FileCopyrightText: 2022 Linaro Ltd. +# SPDX-FileCopyrightText: 2022 Bartosz Golaszewski + +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 index 0000000..bbb962f --- /dev/null +++ b/bindings/rust/libgpiod/src/chip.rs @@ -0,0 +1,301 @@ +// SPDX-License-Identifier: Apache-2.0 OR BSD-3-Clause +// SPDX-FileCopyrightText: 2022 Linaro Ltd. +// SPDX-FileCopyrightText: 2022 Viresh Kumar + +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>(path: &P) -> Result { + // 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::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 { + // 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 { + // 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) -> Result { + 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 { + // 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 { + 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 { + 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 { + // 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 { + 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 index 0000000..7f8f377 --- /dev/null +++ b/bindings/rust/libgpiod/src/edge_event.rs @@ -0,0 +1,97 @@ +// SPDX-License-Identifier: Apache-2.0 OR BSD-3-Clause +// SPDX-FileCopyrightText: 2022 Linaro Ltd. +// SPDX-FileCopyrightText: 2022 Viresh Kumar + +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 { + // 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 { + // 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 index 0000000..68d6e2f --- /dev/null +++ b/bindings/rust/libgpiod/src/event_buffer.rs @@ -0,0 +1,178 @@ +// SPDX-License-Identifier: Apache-2.0 OR BSD-3-Clause +// SPDX-FileCopyrightText: 2022 Linaro Ltd. +// SPDX-FileCopyrightText: 2022 Viresh Kumar + +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 { + 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 { + // 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 { + // 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 { + 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 index 0000000..472c891 --- /dev/null +++ b/bindings/rust/libgpiod/src/info_event.rs @@ -0,0 +1,76 @@ +// SPDX-License-Identifier: Apache-2.0 OR BSD-3-Clause +// SPDX-FileCopyrightText: 2022 Linaro Ltd. +// SPDX-FileCopyrightText: 2022 Viresh Kumar + +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 { + // 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 index 0000000..fd95ed2 --- /dev/null +++ b/bindings/rust/libgpiod/src/lib.rs @@ -0,0 +1,518 @@ +// SPDX-License-Identifier: Apache-2.0 OR BSD-3-Clause +// SPDX-FileCopyrightText: 2022 Linaro Ltd. +// SPDX-FileCopyrightText: 2022 Viresh Kumar +// +// 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 = std::result::Result; + +/// 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; + + /// Maps offsets to Settings + pub type SettingsMap = IntMap; + + impl Value { + pub fn new(val: gpiod::gpiod_line_value) -> Result { + 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 { + 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> { + 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) -> 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 { + 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> { + 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) -> 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), + /// Drive. + Drive(Drive), + /// Edge detection. + EdgeDetection(Option), + /// 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 { + 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 { + 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 { + 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>(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>(path: &P) -> Result> { + 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 index 0000000..d0a4aba --- /dev/null +++ b/bindings/rust/libgpiod/src/line_config.rs @@ -0,0 +1,150 @@ +// SPDX-License-Identifier: Apache-2.0 OR BSD-3-Clause +// SPDX-FileCopyrightText: 2022 Linaro Ltd. +// SPDX-FileCopyrightText: 2022 Viresh Kumar + +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 { + // 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 { + 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 index 0000000..bd290f6 --- /dev/null +++ b/bindings/rust/libgpiod/src/line_info.rs @@ -0,0 +1,226 @@ +// SPDX-License-Identifier: Apache-2.0 OR BSD-3-Clause +// SPDX-FileCopyrightText: 2022 Linaro Ltd. +// SPDX-FileCopyrightText: 2022 Viresh Kumar + +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 { + // 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 { + // 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> { + // 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 { + // 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> { + // 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 { + // 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 index 0000000..a7fe6d0 --- /dev/null +++ b/bindings/rust/libgpiod/src/line_request.rs @@ -0,0 +1,253 @@ +// SPDX-License-Identifier: Apache-2.0 OR BSD-3-Clause +// SPDX-FileCopyrightText: 2022 Linaro Ltd. +// SPDX-FileCopyrightText: 2022 Viresh Kumar + +#[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 { + 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 { + 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 { + // 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 { + 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 { + 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) -> Result { + 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 { + 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 index 0000000..4ba20d4 --- /dev/null +++ b/bindings/rust/libgpiod/src/line_settings.rs @@ -0,0 +1,295 @@ +// SPDX-License-Identifier: Apache-2.0 OR BSD-3-Clause +// SPDX-FileCopyrightText: 2022 Linaro Ltd. +// SPDX-FileCopyrightText: 2022 Viresh Kumar + +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 { + // 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 { + // 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 { + 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 { + // 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) -> 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> { + // 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) -> 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> { + // 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 { + // 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 { + // 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 { + // 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 { + // 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 index 0000000..9b66cc9 --- /dev/null +++ b/bindings/rust/libgpiod/src/request_config.rs @@ -0,0 +1,100 @@ +// SPDX-License-Identifier: Apache-2.0 OR BSD-3-Clause +// SPDX-FileCopyrightText: 2022 Linaro Ltd. +// SPDX-FileCopyrightText: 2022 Viresh Kumar + +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 { + // 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 index 0000000..8927649 --- /dev/null +++ b/bindings/rust/libgpiod/tests/Makefile.am @@ -0,0 +1,15 @@ +# SPDX-License-Identifier: GPL-2.0-or-later +# SPDX-FileCopyrightText: 2022 Linaro Ltd. +# SPDX-FileCopyrightText: 2022 Bartosz Golaszewski + +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 index 0000000..60b4ecc --- /dev/null +++ b/bindings/rust/libgpiod/tests/chip.rs @@ -0,0 +1,97 @@ +// SPDX-License-Identifier: Apache-2.0 OR BSD-3-Clause +// SPDX-FileCopyrightText: 2022 Linaro Ltd. +// SPDX-FileCopyrightText: 2022 Viresh Kumar + +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 index 0000000..6a32db4 --- /dev/null +++ b/bindings/rust/libgpiod/tests/common/Makefile.am @@ -0,0 +1,5 @@ +# SPDX-License-Identifier: GPL-2.0-or-later +# SPDX-FileCopyrightText: 2022 Linaro Ltd. +# SPDX-FileCopyrightText: 2022 Bartosz Golaszewski + +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 index 0000000..7bb1f65 --- /dev/null +++ b/bindings/rust/libgpiod/tests/common/config.rs @@ -0,0 +1,142 @@ +// SPDX-License-Identifier: Apache-2.0 OR BSD-3-Clause +// SPDX-FileCopyrightText: 2022 Linaro Ltd. +// SPDX-FileCopyrightText: 2022 Viresh Kumar + +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>, + chip: Option, + request: Option, + rconfig: request::Config, + lconfig: line::Config, + lsettings: Option, +} + +impl TestConfig { + pub(crate) fn new(ngpio: usize) -> Result { + 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, val: Option) { + 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) { + 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, edge: Option) { + 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> { + self.sim.clone() + } + + pub(crate) fn sim_val(&self, offset: Offset) -> Result { + 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 index 0000000..586115b --- /dev/null +++ b/bindings/rust/libgpiod/tests/common/mod.rs @@ -0,0 +1,9 @@ +// SPDX-License-Identifier: Apache-2.0 OR BSD-3-Clause +// SPDX-FileCopyrightText: 2022 Linaro Ltd. +// SPDX-FileCopyrightText: 2022 Viresh Kumar + +#[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 index 0000000..03b7e7c --- /dev/null +++ b/bindings/rust/libgpiod/tests/edge_event.rs @@ -0,0 +1,297 @@ +// SPDX-License-Identifier: Apache-2.0 OR BSD-3-Clause +// SPDX-FileCopyrightText: 2022 Linaro Ltd. +// SPDX-FileCopyrightText: 2022 Viresh Kumar + +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>, 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>, 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>, 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 index 0000000..c969af7 --- /dev/null +++ b/bindings/rust/libgpiod/tests/info_event.rs @@ -0,0 +1,166 @@ +// SPDX-License-Identifier: Apache-2.0 OR BSD-3-Clause +// SPDX-FileCopyrightText: 2022 Linaro Ltd. +// SPDX-FileCopyrightText: 2022 Viresh Kumar + +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>, 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 index 0000000..7fccf8c --- /dev/null +++ b/bindings/rust/libgpiod/tests/line_config.rs @@ -0,0 +1,141 @@ +// SPDX-License-Identifier: Apache-2.0 OR BSD-3-Clause +// SPDX-FileCopyrightText: 2022 Linaro Ltd. +// SPDX-FileCopyrightText: 2022 Viresh Kumar + +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 index 0000000..d02c9ea --- /dev/null +++ b/bindings/rust/libgpiod/tests/line_info.rs @@ -0,0 +1,328 @@ +// SPDX-License-Identifier: Apache-2.0 OR BSD-3-Clause +// SPDX-FileCopyrightText: 2022 Linaro Ltd. +// SPDX-FileCopyrightText: 2022 Viresh Kumar + +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 index 0000000..a936a1b --- /dev/null +++ b/bindings/rust/libgpiod/tests/line_request.rs @@ -0,0 +1,517 @@ +// SPDX-License-Identifier: Apache-2.0 OR BSD-3-Clause +// SPDX-FileCopyrightText: 2022 Linaro Ltd. +// SPDX-FileCopyrightText: 2022 Viresh Kumar + +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 index 0000000..1aaa6b4 --- /dev/null +++ b/bindings/rust/libgpiod/tests/line_settings.rs @@ -0,0 +1,203 @@ +// SPDX-License-Identifier: Apache-2.0 OR BSD-3-Clause +// SPDX-FileCopyrightText: 2022 Linaro Ltd. +// SPDX-FileCopyrightText: 2022 Viresh Kumar + +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 index 0000000..a925a68 --- /dev/null +++ b/bindings/rust/libgpiod/tests/request_config.rs @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: Apache-2.0 OR BSD-3-Clause +// SPDX-FileCopyrightText: 2022 Linaro Ltd. +// SPDX-FileCopyrightText: 2022 Viresh Kumar + +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 index 0000000..d0b1479 --- /dev/null +++ b/configure.ac @@ -0,0 +1,300 @@ +# SPDX-License-Identifier: GPL-2.0-or-later +# SPDX-FileCopyrightText: 2017-2022 Bartosz Golaszewski + +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 index 0000000..fbc2196 --- /dev/null +++ b/contrib/Android.bp @@ -0,0 +1,136 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +// SPDX-FileCopyrightText: 2023 Benjamin Li + +// 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 index 0000000..1b50e86 --- /dev/null +++ b/contrib/Makefile.am @@ -0,0 +1,4 @@ +# SPDX-License-Identifier: GPL-2.0-or-later +# SPDX-FileCopyrightText: 2023 Bartosz Golaszewski + +EXTRA_DIST = Android.bp diff --git a/examples/.gitignore b/examples/.gitignore new file mode 100644 index 0000000..8fd3ff3 --- /dev/null +++ b/examples/.gitignore @@ -0,0 +1,16 @@ +# SPDX-License-Identifier: GPL-2.0-or-later +# SPDX-FileCopyrightText: 2023 Kent Gibson + +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 index 0000000..ed01dbc --- /dev/null +++ b/examples/Makefile.am @@ -0,0 +1,22 @@ +# SPDX-License-Identifier: GPL-2.0-or-later +# SPDX-FileCopyrightText: 2023 Kent Gibson + +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 index 0000000..8b1d643 --- /dev/null +++ b/examples/async_watch_line_value.c @@ -0,0 +1,143 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +// SPDX-FileCopyrightText: 2023 Kent Gibson + +/* Minimal example of asynchronously watching for edges on a single line. */ + +#include +#include +#include +#include +#include +#include + +/* 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 index 0000000..346d258 --- /dev/null +++ b/examples/find_line_by_name.c @@ -0,0 +1,105 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +// SPDX-FileCopyrightText: 2023 Kent Gibson + +/* Minimal example of finding a line with the given name. */ + +#include +#include +#include +#include +#include +#include +#include + +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 index 0000000..5c181c8 --- /dev/null +++ b/examples/get_chip_info.c @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +// SPDX-FileCopyrightText: 2023 Kent Gibson + +/* Minimal example of reading the info for a chip. */ + +#include +#include +#include +#include +#include + +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 index 0000000..ad28686 --- /dev/null +++ b/examples/get_line_info.c @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +// SPDX-FileCopyrightText: 2023 Kent Gibson + +/* Minimal example of reading the info for a line. */ + +#include +#include +#include +#include +#include + +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 index 0000000..1de9901 --- /dev/null +++ b/examples/get_line_value.c @@ -0,0 +1,106 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +// SPDX-FileCopyrightText: 2023 Kent Gibson + +/* Minimal example of reading a single line. */ + +#include +#include +#include +#include +#include + +/* 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 index 0000000..c6df3f6 --- /dev/null +++ b/examples/get_multiple_line_values.c @@ -0,0 +1,119 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +// SPDX-FileCopyrightText: 2023 Kent Gibson + +/* Minimal example of reading multiple lines. */ + +#include +#include +#include +#include +#include + +#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 index 0000000..451bb0e --- /dev/null +++ b/examples/reconfigure_input_to_output.c @@ -0,0 +1,155 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +// SPDX-FileCopyrightText: 2023 Kent Gibson + +/* + * Example of a bi-directional line requested as input and then switched + * to output. + */ + +#include +#include +#include +#include +#include + +/* 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 index 0000000..6e522d6 --- /dev/null +++ b/examples/toggle_line_value.c @@ -0,0 +1,113 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +// SPDX-FileCopyrightText: 2023 Kent Gibson + +/* Minimal example of toggling a single line. */ + +#include +#include +#include +#include +#include +#include + +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 index 0000000..824d5ea --- /dev/null +++ b/examples/toggle_multiple_line_values.c @@ -0,0 +1,136 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +// SPDX-FileCopyrightText: 2023 Kent Gibson + +/* Minimal example of toggling multiple lines. */ + +#include +#include +#include +#include +#include +#include + +#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 index 0000000..9df3121 --- /dev/null +++ b/examples/watch_line_info.c @@ -0,0 +1,73 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +// SPDX-FileCopyrightText: 2023 Kent Gibson + +/* Minimal example of watching for info changes on particular lines. */ + +#include +#include +#include +#include +#include +#include + +#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 index 0000000..062a46a --- /dev/null +++ b/examples/watch_line_rising.c @@ -0,0 +1,129 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +// SPDX-FileCopyrightText: 2023 Kent Gibson + +/* Minimal example of watching for rising edges on a single line. */ + +#include +#include +#include +#include +#include + +/* 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 index 0000000..879b09b --- /dev/null +++ b/examples/watch_line_value.c @@ -0,0 +1,133 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +// SPDX-FileCopyrightText: 2023 Kent Gibson + +/* Minimal example of watching for edges on a single line. */ + +#include +#include +#include +#include +#include + +/* 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 index 0000000..e955b2c --- /dev/null +++ b/examples/watch_multiple_line_values.c @@ -0,0 +1,140 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +// SPDX-FileCopyrightText: 2023 Kent Gibson + +/* Minimal example of watching for edges on multiple lines. */ + +#include +#include +#include +#include +#include + +#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 index 0000000..7f986ec --- /dev/null +++ b/include/Makefile.am @@ -0,0 +1,4 @@ +# SPDX-License-Identifier: GPL-2.0-or-later +# SPDX-FileCopyrightText: 2017-2021 Bartosz Golaszewski + +include_HEADERS = gpiod.h diff --git a/include/gpiod.h b/include/gpiod.h new file mode 100644 index 0000000..d86c6ac --- /dev/null +++ b/include/gpiod.h @@ -0,0 +1,1366 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +/* SPDX-FileCopyrightText: 2017-2022 Bartosz Golaszewski */ + +/** + * @file gpiod.h + */ + +#ifndef __LIBGPIOD_GPIOD_H__ +#define __LIBGPIOD_GPIOD_H__ + +#include +#include +#include + +#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 index 0000000..3e7114b --- /dev/null +++ b/lib/Makefile.am @@ -0,0 +1,28 @@ +# SPDX-License-Identifier: GPL-2.0-or-later +# SPDX-FileCopyrightText: 2017-2021 Bartosz Golaszewski + +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 index 0000000..2d9f44d --- /dev/null +++ b/lib/chip-info.c @@ -0,0 +1,73 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later +// SPDX-FileCopyrightText: 2022 Bartosz Golaszewski + +#include +#include +#include +#include + +#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 index 0000000..611eb32 --- /dev/null +++ b/lib/chip.c @@ -0,0 +1,251 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later +// SPDX-FileCopyrightText: 2021 Bartosz Golaszewski + +#include +#include +#include +#include +#include +#include +#include + +#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 index 0000000..8ec355f --- /dev/null +++ b/lib/edge-event.c @@ -0,0 +1,212 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later +// SPDX-FileCopyrightText: 2021 Bartosz Golaszewski + +#include +#include +#include +#include +#include +#include + +#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 index 0000000..0e3ef9b --- /dev/null +++ b/lib/info-event.c @@ -0,0 +1,105 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later +// SPDX-FileCopyrightText: 2021 Bartosz Golaszewski + +#include +#include +#include +#include +#include + +#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 index 0000000..56cb8b9 --- /dev/null +++ b/lib/internal.c @@ -0,0 +1,158 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later +// SPDX-FileCopyrightText: 2021-2022 Bartosz Golaszewski + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#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 index 0000000..420fbdd --- /dev/null +++ b/lib/internal.h @@ -0,0 +1,48 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +/* SPDX-FileCopyrightText: 2021 Bartosz Golaszewski */ + +#ifndef __LIBGPIOD_GPIOD_INTERNAL_H__ +#define __LIBGPIOD_GPIOD_INTERNAL_H__ + +#include +#include +#include + +#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 index 0000000..913859a --- /dev/null +++ b/lib/libgpiod.pc.in @@ -0,0 +1,15 @@ +# SPDX-License-Identifier: GPL-2.0-or-later +# SPDX-FileCopyrightText: 2017-2021 Bartosz Golaszewski +# SPDX-FileCopyrightText: 2018 Clemens Gruber + +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 index 0000000..9302c1b --- /dev/null +++ b/lib/line-config.c @@ -0,0 +1,549 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later +// SPDX-FileCopyrightText: 2022 Bartosz Golaszewski + +#include +#include +#include +#include +#include +#include + +#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 index 0000000..c61b789 --- /dev/null +++ b/lib/line-info.c @@ -0,0 +1,211 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later +// SPDX-FileCopyrightText: 2021 Bartosz Golaszewski + +#include +#include +#include +#include + +#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 index 0000000..b76b3d7 --- /dev/null +++ b/lib/line-request.c @@ -0,0 +1,307 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later +// SPDX-FileCopyrightText: 2021 Bartosz Golaszewski + +#include +#include +#include +#include +#include +#include +#include + +#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 index 0000000..e54353f --- /dev/null +++ b/lib/line-settings.c @@ -0,0 +1,266 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later +// SPDX-FileCopyrightText: 2022 Bartosz Golaszewski + +#include +#include +#include +#include +#include + +#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 index 0000000..e109f80 --- /dev/null +++ b/lib/misc.c @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later +// SPDX-FileCopyrightText: 2017-2022 Bartosz Golaszewski + +#include + +#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 index 0000000..da055c5 --- /dev/null +++ b/lib/request-config.c @@ -0,0 +1,79 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later +// SPDX-FileCopyrightText: 2021 Bartosz Golaszewski + +#include +#include +#include +#include +#include + +#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 index 0000000..cb9966d --- /dev/null +++ b/lib/uapi/gpio.h @@ -0,0 +1,531 @@ +/* SPDX-License-Identifier: GPL-2.0-only WITH Linux-syscall-note */ +/* + * - 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 +#include +#include + +/* + * 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 index 0000000..1263cf7 --- /dev/null +++ b/man/.gitignore @@ -0,0 +1,4 @@ +# SPDX-License-Identifier: GPL-2.0-or-later +# SPDX-FileCopyrightText: 2017-2021 Bartosz Golaszewski + +*.man diff --git a/man/Makefile.am b/man/Makefile.am new file mode 100644 index 0000000..ddd0628 --- /dev/null +++ b/man/Makefile.am @@ -0,0 +1,22 @@ +# SPDX-License-Identifier: GPL-2.0-or-later +# SPDX-FileCopyrightText: 2017-2021 Bartosz Golaszewski + +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 index 0000000..0602b6b --- /dev/null +++ b/man/template @@ -0,0 +1,10 @@ +# SPDX-License-Identifier: CC-BY-SA-4.0 +# SPDX-FileCopyrightText: 2017-2021 Bartosz Golaszewski + +[AUTHOR] +Bartosz Golaszewski + +[REPORTING BUGS] +Report bugs to: + Bartosz Golaszewski + linux-gpio diff --git a/sphinx/conf.py b/sphinx/conf.py new file mode 100644 index 0000000..51ae3e9 --- /dev/null +++ b/sphinx/conf.py @@ -0,0 +1,66 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +# SPDX-FileCopyrightText: 2022 Kent Gibson + +# 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 index 0000000..c26d068 --- /dev/null +++ b/sphinx/index.rst @@ -0,0 +1,24 @@ +.. + SPDX-License-Identifier: LGPL-2.1-or-later + SPDX-FileCopyrightText: 2022 Kent Gibson + +.. + 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 index 0000000..676f092 --- /dev/null +++ b/tests/.gitignore @@ -0,0 +1,4 @@ +# SPDX-License-Identifier: GPL-2.0-or-later +# SPDX-FileCopyrightText: 2017-2021 Bartosz Golaszewski + +gpiod-test diff --git a/tests/Makefile.am b/tests/Makefile.am new file mode 100644 index 0000000..0680d5e --- /dev/null +++ b/tests/Makefile.am @@ -0,0 +1,32 @@ +# SPDX-License-Identifier: GPL-2.0-or-later +# SPDX-FileCopyrightText: 2017-2022 Bartosz Golaszewski + +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 index 0000000..7e5b396 --- /dev/null +++ b/tests/gpiod-test-helpers.c @@ -0,0 +1,41 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* SPDX-FileCopyrightText: 2017-2022 Bartosz Golaszewski */ + +/* + * 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 index 0000000..41791a3 --- /dev/null +++ b/tests/gpiod-test-helpers.h @@ -0,0 +1,203 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* SPDX-FileCopyrightText: 2022 Bartosz Golaszewski */ + +#ifndef __GPIOD_TEST_HELPERS_H__ +#define __GPIOD_TEST_HELPERS_H__ + +#include +#include +#include + +#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 index 0000000..ac6c71a --- /dev/null +++ b/tests/gpiod-test-sim.c @@ -0,0 +1,464 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +/* SPDX-FileCopyrightText: 2022 Bartosz Golaszewski */ + +#include +#include +#include +#include + +#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 index 0000000..f6a4bf0 --- /dev/null +++ b/tests/gpiod-test-sim.h @@ -0,0 +1,79 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +/* SPDX-FileCopyrightText: 2022 Bartosz Golaszewski */ + +#ifndef __GPIOD_TEST_SIM_H__ +#define __GPIOD_TEST_SIM_H__ + +#include +#include +#include + +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 index 0000000..4e65ae2 --- /dev/null +++ b/tests/gpiod-test.c @@ -0,0 +1,90 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +// SPDX-FileCopyrightText: 2017-2022 Bartosz Golaszewski + +#include +#include +#include +#include +#include +#include + +#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 index 0000000..6a84162 --- /dev/null +++ b/tests/gpiod-test.h @@ -0,0 +1,45 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* SPDX-FileCopyrightText: 2017-2022 Bartosz Golaszewski */ + +/* + * 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 + +/* 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 index 0000000..5731644 --- /dev/null +++ b/tests/gpiosim/.gitignore @@ -0,0 +1,4 @@ +# SPDX-License-Identifier: GPL-2.0-or-later +# SPDX-FileCopyrightText: 2017-2022 Bartosz Golaszewski + +gpiosim-selftest diff --git a/tests/gpiosim/Makefile.am b/tests/gpiosim/Makefile.am new file mode 100644 index 0000000..5888873 --- /dev/null +++ b/tests/gpiosim/Makefile.am @@ -0,0 +1,15 @@ +# SPDX-License-Identifier: GPL-2.0-or-later +# SPDX-FileCopyrightText: 2021-2022 Bartosz Golaszewski + +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 index 0000000..ce6beee --- /dev/null +++ b/tests/gpiosim/gpiosim-selftest.c @@ -0,0 +1,157 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +// SPDX-FileCopyrightText: 2021-2022 Bartosz Golaszewski + +#include +#include + +#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 index 0000000..fca6b7f --- /dev/null +++ b/tests/gpiosim/gpiosim.c @@ -0,0 +1,1178 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later +// SPDX-FileCopyrightText: 2021-2022 Bartosz Golaszewski + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#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 index 0000000..7d75852 --- /dev/null +++ b/tests/gpiosim/gpiosim.h @@ -0,0 +1,76 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +/* SPDX-FileCopyrightText: 2021-2022 Bartosz Golaszewski */ + +#ifndef __GPIOD_GPIOSIM_H__ +#define __GPIOD_GPIOSIM_H__ + +#include +#include + +#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 index 0000000..db76385 --- /dev/null +++ b/tests/tests-chip-info.c @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +// SPDX-FileCopyrightText: 2017-2022 Bartosz Golaszewski + +#include +#include +#include + +#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 index 0000000..815b4c7 --- /dev/null +++ b/tests/tests-chip.c @@ -0,0 +1,199 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +// SPDX-FileCopyrightText: 2017-2021 Bartosz Golaszewski + +#include +#include +#include + +#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 index 0000000..b744ca5 --- /dev/null +++ b/tests/tests-edge-event.c @@ -0,0 +1,683 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +// SPDX-FileCopyrightText: 2017-2021 Bartosz Golaszewski + +#include +#include +#include + +#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 index 0000000..cbd9e9e --- /dev/null +++ b/tests/tests-info-event.c @@ -0,0 +1,299 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +// SPDX-FileCopyrightText: 2017-2021 Bartosz Golaszewski + +#include +#include +#include + +#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 index 0000000..469500b --- /dev/null +++ b/tests/tests-line-config.c @@ -0,0 +1,470 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +// SPDX-FileCopyrightText: 2017-2021 Bartosz Golaszewski + +#include +#include +#include + +#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 index 0000000..cf2c650 --- /dev/null +++ b/tests/tests-line-info.c @@ -0,0 +1,407 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +// SPDX-FileCopyrightText: 2017-2021 Bartosz Golaszewski + +#include +#include +#include + +#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 index 0000000..7bba078 --- /dev/null +++ b/tests/tests-line-request.c @@ -0,0 +1,701 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +// SPDX-FileCopyrightText: 2017-2021 Bartosz Golaszewski + +#include +#include + +#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 index 0000000..b86fd26 --- /dev/null +++ b/tests/tests-line-settings.c @@ -0,0 +1,316 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +// SPDX-FileCopyrightText: 2022 Bartosz Golaszewski + +#include +#include +#include + +#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 index 0000000..240dd02 --- /dev/null +++ b/tests/tests-misc.c @@ -0,0 +1,99 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +// SPDX-FileCopyrightText: 2017-2021 Bartosz Golaszewski + +#include +#include +#include +#include + +#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 index 0000000..d3c679a --- /dev/null +++ b/tests/tests-request-config.c @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +// SPDX-FileCopyrightText: 2017-2021 Bartosz Golaszewski + +#include +#include + +#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 index 0000000..dfdbc0d --- /dev/null +++ b/tools/.gitignore @@ -0,0 +1,9 @@ +# SPDX-License-Identifier: GPL-2.0-or-later +# SPDX-FileCopyrightText: 2017-2021 Bartosz Golaszewski + +gpiodetect +gpioinfo +gpioget +gpioset +gpiomon +gpionotify diff --git a/tools/Makefile.am b/tools/Makefile.am new file mode 100644 index 0000000..bf6170a --- /dev/null +++ b/tools/Makefile.am @@ -0,0 +1,25 @@ +# SPDX-License-Identifier: GPL-2.0-or-later +# SPDX-FileCopyrightText: 2017-2021 Bartosz Golaszewski + +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 index 0000000..b55c5eb --- /dev/null +++ b/tools/gpio-tools-test.bash @@ -0,0 +1,3090 @@ +#!/bin/bash +# SPDX-License-Identifier: GPL-2.0-or-later +# SPDX-FileCopyrightText: 2017-2021 Bartosz Golaszewski +# SPDX-FileCopyrightText: 2022 Kent Gibson +# SPDX-FileCopyrightText: 2023 Bartosz Golaszewski + +# 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 \.\.\..*" + output_regex_match ".*toggle \[line\]\.\.\..*" + output_regex_match ".*sleep .*" +} + +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 index 0000000..0a3461b --- /dev/null +++ b/tools/gpiodetect.c @@ -0,0 +1,124 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +// SPDX-FileCopyrightText: 2017-2021 Bartosz Golaszewski +// SPDX-FileCopyrightText: 2022 Kent Gibson + +#include +#include +#include +#include +#include + +#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 index 0000000..f611737 --- /dev/null +++ b/tools/gpioget.c @@ -0,0 +1,243 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +// SPDX-FileCopyrightText: 2017-2021 Bartosz Golaszewski +// SPDX-FileCopyrightText: 2022 Kent Gibson + +#include +#include +#include +#include +#include +#include + +#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] ...\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 \trestrict scope to a particular chip\n"); + printf(" -C, --consumer \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 \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 index 0000000..d5e4751 --- /dev/null +++ b/tools/gpioinfo.c @@ -0,0 +1,271 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +// SPDX-FileCopyrightText: 2017-2021 Bartosz Golaszewski +// SPDX-FileCopyrightText: 2022 Kent Gibson + +#include +#include +#include +#include +#include +#include + +#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 \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("ed_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 index 0000000..e3abb2d --- /dev/null +++ b/tools/gpiomon.c @@ -0,0 +1,499 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +// SPDX-FileCopyrightText: 2017-2021 Bartosz Golaszewski +// SPDX-FileCopyrightText: 2022 Kent Gibson + +#include +#include +#include +#include +#include +#include +#include + +#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] ...\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 \trestrict scope to a particular chip\n"); + printf(" -C, --consumer \tconsumer name applied to requested lines (default is 'gpiomon')\n"); + printf(" -e, --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 \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 \tspecify a custom output format\n"); + printf(" --idle-timeout \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 \n"); + printf("\t\t\texit after processing num events\n"); + printf(" -p, --debounce-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 index 0000000..2c56590 --- /dev/null +++ b/tools/gpionotify.c @@ -0,0 +1,463 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +// SPDX-FileCopyrightText: 2022 Kent Gibson + +#include +#include +#include +#include +#include +#include +#include +#include + +#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] ...\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 \trestrict scope to a particular chip\n"); + printf(" -e, --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 \tspecify a custom output format\n"); + printf(" --idle-timeout \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 \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 index 0000000..9dc5aeb --- /dev/null +++ b/tools/gpioset.c @@ -0,0 +1,1008 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +// SPDX-FileCopyrightText: 2017-2021 Bartosz Golaszewski +// SPDX-FileCopyrightText: 2022 Kent Gibson + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#ifdef GPIOSET_INTERACTIVE +#include +#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] ...\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 \trestrict scope to a particular chip\n"); + printf(" -C, --consumer \tconsumer name applied to requested lines (default is 'gpioset')\n"); + printf(" -d, --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 \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]...\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 ...\n"); + printf(" Update the output values of the given requested lines\n\n"); + printf(" sleep \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 index 0000000..64592d3 --- /dev/null +++ b/tools/tools-common.c @@ -0,0 +1,783 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +// SPDX-FileCopyrightText: 2017-2021 Bartosz Golaszewski +// SPDX-FileCopyrightText: 2022 Kent Gibson + +/* Common code for GPIO tools. */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#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 \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 index 0000000..c82317a --- /dev/null +++ b/tools/tools-common.h @@ -0,0 +1,123 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* SPDX-FileCopyrightText: 2017-2021 Bartosz Golaszewski */ +/* SPDX-FileCopyrightText: 2022 Kent Gibson */ + +#ifndef __GPIOD_TOOLS_COMMON_H__ +#define __GPIOD_TOOLS_COMMON_H__ + +#include + +/* + * 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__ */