├── .coveragerc ├── .gitignore ├── .pylintrc ├── .readthedocs.yml ├── .style.yapf ├── .travis.yml ├── CHEATSHEET.rst ├── LICENSE ├── LICENSE.APACHE2 ├── LICENSE.MIT ├── MANIFEST.in ├── Makefile ├── README.rst ├── asyncgpio ├── __init__.py ├── gpio.py ├── libgpiod.py └── test.py ├── ci ├── rtd-requirements.txt ├── test-requirements.txt └── travis.sh ├── docs ├── Makefile ├── make.bat └── source │ ├── _static │ └── .gitkeep │ ├── conf.py │ ├── history.rst │ ├── index.rst │ └── usage.rst ├── examples ├── line_echo.py ├── line_echo_polled.py ├── line_value.py └── push_button_event.py ├── newsfragments ├── .gitkeep └── README.rst ├── pyproject.toml ├── setup.cfg ├── setup.py └── tests ├── __init__.py ├── run.py └── test.sh /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | branch=True 3 | source=asyncgpio 4 | 5 | [report] 6 | precision = 1 7 | exclude_lines = 8 | pragma: no cover 9 | abc.abstractmethod 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Add any project-specific files here: 2 | 3 | 4 | # Sphinx docs 5 | docs/build/ 6 | 7 | # Byte-compiled / optimized / DLL files 8 | __pycache__/ 9 | *.py[cod] 10 | *~ 11 | \#* 12 | .#* 13 | 14 | # C extensions 15 | *.so 16 | 17 | # Distribution / packaging 18 | .Python 19 | /build/ 20 | /develop-eggs/ 21 | /dist/ 22 | /eggs/ 23 | /lib/ 24 | /lib64/ 25 | /parts/ 26 | /sdist/ 27 | /var/ 28 | *.egg-info/ 29 | .installed.cfg 30 | *.egg 31 | /.pytest_cache/ 32 | 33 | # Installer logs 34 | pip-log.txt 35 | 36 | # Unit test / coverage reports 37 | htmlcov/ 38 | .tox/ 39 | .coverage 40 | .coverage.* 41 | .cache 42 | nosetests.xml 43 | coverage.xml 44 | 45 | # Translations 46 | *.mo 47 | 48 | # Mr Developer 49 | .mr.developer.cfg 50 | .project 51 | .pydevproject 52 | 53 | # Rope 54 | .ropeproject 55 | 56 | # Django stuff: 57 | *.log 58 | *.pot 59 | /.pybuild/ 60 | -------------------------------------------------------------------------------- /.pylintrc: -------------------------------------------------------------------------------- 1 | [MESSAGES CONTROL] 2 | disable=bad-continuation,invalid-name,unnecessary-pass,protected-access,fixme,broad-except,no-absolute-import,global-statement,C,R 3 | 4 | -------------------------------------------------------------------------------- /.readthedocs.yml: -------------------------------------------------------------------------------- 1 | # https://docs.readthedocs.io/en/latest/yaml-config.html 2 | formats: 3 | - htmlzip 4 | - epub 5 | 6 | requirements_file: ci/rtd-requirements.txt 7 | 8 | python: 9 | version: 3 10 | pip_install: True 11 | -------------------------------------------------------------------------------- /.style.yapf: -------------------------------------------------------------------------------- 1 | [style] 2 | # Align closing bracket with visual indentation. 3 | align_closing_bracket_with_visual_indent=True 4 | 5 | # Allow dictionary keys to exist on multiple lines. For example: 6 | # 7 | # x = { 8 | # ('this is the first element of a tuple', 9 | # 'this is the second element of a tuple'): 10 | # value, 11 | # } 12 | allow_multiline_dictionary_keys=False 13 | 14 | # Allow lambdas to be formatted on more than one line. 15 | allow_multiline_lambdas=False 16 | 17 | # Insert a blank line before a class-level docstring. 18 | blank_line_before_class_docstring=False 19 | 20 | # Insert a blank line before a 'def' or 'class' immediately nested 21 | # within another 'def' or 'class'. For example: 22 | # 23 | # class Foo: 24 | # # <------ this blank line 25 | # def method(): 26 | # ... 27 | blank_line_before_nested_class_or_def=False 28 | 29 | # Do not split consecutive brackets. Only relevant when 30 | # dedent_closing_brackets is set. For example: 31 | # 32 | # call_func_that_takes_a_dict( 33 | # { 34 | # 'key1': 'value1', 35 | # 'key2': 'value2', 36 | # } 37 | # ) 38 | # 39 | # would reformat to: 40 | # 41 | # call_func_that_takes_a_dict({ 42 | # 'key1': 'value1', 43 | # 'key2': 'value2', 44 | # }) 45 | coalesce_brackets=False 46 | 47 | # The column limit. 48 | column_limit=99 49 | 50 | # Indent width used for line continuations. 51 | continuation_indent_width=4 52 | 53 | # Put closing brackets on a separate line, dedented, if the bracketed 54 | # expression can't fit in a single line. Applies to all kinds of brackets, 55 | # including function definitions and calls. For example: 56 | # 57 | # config = { 58 | # 'key1': 'value1', 59 | # 'key2': 'value2', 60 | # } # <--- this bracket is dedented and on a separate line 61 | # 62 | # time_series = self.remote_client.query_entity_counters( 63 | # entity='dev3246.region1', 64 | # key='dns.query_latency_tcp', 65 | # transform=Transformation.AVERAGE(window=timedelta(seconds=60)), 66 | # start_ts=now()-timedelta(days=3), 67 | # end_ts=now(), 68 | # ) # <--- this bracket is dedented and on a separate line 69 | dedent_closing_brackets=True 70 | 71 | # Place each dictionary entry onto its own line. 72 | each_dict_entry_on_separate_line=True 73 | 74 | # The regex for an i18n comment. The presence of this comment stops 75 | # reformatting of that line, because the comments are required to be 76 | # next to the string they translate. 77 | i18n_comment= 78 | 79 | # The i18n function call names. The presence of this function stops 80 | # reformattting on that line, because the string it has cannot be moved 81 | # away from the i18n comment. 82 | i18n_function_call= 83 | 84 | # Indent the dictionary value if it cannot fit on the same line as the 85 | # dictionary key. For example: 86 | # 87 | # config = { 88 | # 'key1': 89 | # 'value1', 90 | # 'key2': value1 + 91 | # value2, 92 | # } 93 | indent_dictionary_value=True 94 | 95 | # The number of columns to use for indentation. 96 | indent_width=4 97 | 98 | # Join short lines into one line. E.g., single line 'if' statements. 99 | join_multiple_lines=False 100 | 101 | # Use spaces around default or named assigns. 102 | spaces_around_default_or_named_assign=False 103 | 104 | # Use spaces around the power operator. 105 | spaces_around_power_operator=False 106 | 107 | # The number of spaces required before a trailing comment. 108 | spaces_before_comment=2 109 | 110 | # Insert a space between the ending comma and closing bracket of a list, 111 | # etc. 112 | space_between_ending_comma_and_closing_bracket=False 113 | 114 | # Split before arguments if the argument list is terminated by a 115 | # comma. 116 | split_arguments_when_comma_terminated=True 117 | 118 | # Set to True to prefer splitting before '&', '|' or '^' rather than 119 | # after. 120 | split_before_bitwise_operator=True 121 | 122 | # Split before a dictionary or set generator (comp_for). For example, note 123 | # the split before the 'for': 124 | # 125 | # foo = { 126 | # variable: 'Hello world, have a nice day!' 127 | # for variable in bar if variable != 42 128 | # } 129 | split_before_dict_set_generator=True 130 | 131 | # If an argument / parameter list is going to be split, then split before 132 | # the first argument. 133 | split_before_first_argument=True 134 | 135 | # Set to True to prefer splitting before 'and' or 'or' rather than 136 | # after. 137 | split_before_logical_operator=True 138 | 139 | # Split named assignments onto individual lines. 140 | split_before_named_assigns=True 141 | 142 | # The penalty for splitting right after the opening bracket. 143 | split_penalty_after_opening_bracket=30 144 | 145 | # The penalty for splitting the line after a unary operator. 146 | split_penalty_after_unary_operator=10000 147 | 148 | # The penalty for splitting right before an if expression. 149 | split_penalty_before_if_expr=0 150 | 151 | # The penalty of splitting the line around the '&', '|', and '^' 152 | # operators. 153 | split_penalty_bitwise_operator=300 154 | 155 | # The penalty for characters over the column limit. 156 | split_penalty_excess_character=4500 157 | 158 | # The penalty incurred by adding a line split to the unwrapped line. The 159 | # more line splits added the higher the penalty. 160 | split_penalty_for_added_line_split=30 161 | 162 | # The penalty of splitting a list of "import as" names. For example: 163 | # 164 | # from a_very_long_or_indented_module_name_yada_yad import (long_argument_1, 165 | # long_argument_2, 166 | # long_argument_3) 167 | # 168 | # would reformat to something like: 169 | # 170 | # from a_very_long_or_indented_module_name_yada_yad import ( 171 | # long_argument_1, long_argument_2, long_argument_3) 172 | split_penalty_import_names=0 173 | 174 | # The penalty of splitting the line around the 'and' and 'or' 175 | # operators. 176 | split_penalty_logical_operator=0 177 | 178 | # Use the Tab character for indentation. 179 | use_tabs=False 180 | 181 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - 3.6 4 | sudo: false 5 | dist: trusty 6 | ## we need that for actual tests 7 | #addons: 8 | # apt: 9 | # packages: 10 | # - libgpiod1 11 | 12 | env: 13 | - CHECK_DOCS=1 14 | - CHECK_FORMATTING=1 15 | 16 | script: 17 | - ci/travis.sh 18 | -------------------------------------------------------------------------------- /CHEATSHEET.rst: -------------------------------------------------------------------------------- 1 | Tips 2 | ==== 3 | 4 | How to test 5 | ----------- 6 | 7 | There aren't any test cases yet. The reason is that most Linux distributions 8 | don't ship the ``gpio-mockup`` module that would allow me to control 9 | pseudo-GPIO pins from user space. Also, libgpiod isn't in Ubuntu stable yet. 10 | 11 | You can run the example programs on a Raspberry Pi, if you connect the right 12 | two pins on the expansion header. 13 | 14 | 15 | To run yapf 16 | ----------- 17 | 18 | * Show what changes yapf wants to make: ``yapf -rpd setup.py 19 | asyncgpio tests`` 20 | 21 | * Apply all changes directly to the source tree: ``yapf -rpi setup.py 22 | asyncgpio tests`` 23 | 24 | 25 | To make a release 26 | ----------------- 27 | 28 | * Update the version in ``asyncgpio/_version.py`` 29 | 30 | * Run ``towncrier`` to collect your release notes. 31 | 32 | * Review your release notes. 33 | 34 | * Check everything in. 35 | 36 | * Double-check it all works, docs build, etc. 37 | 38 | * Build your sdist and wheel: ``python setup.py sdist bdist_wheel`` 39 | 40 | * Upload to PyPI: ``twine upload dist/*`` 41 | 42 | * Use ``git tag`` to tag your version. 43 | 44 | * Don't forget to ``git push --tags``. 45 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This software is made available under the terms of *either* of the 2 | licenses found in LICENSE.APACHE2 or LICENSE.MIT. Contributions to are 3 | made under the terms of *both* these licenses. 4 | -------------------------------------------------------------------------------- /LICENSE.APACHE2: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /LICENSE.MIT: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.rst CHEATSHEET.rst LICENSE* CODE_OF_CONDUCT* CONTRIBUTING* 2 | include .coveragerc .style.yapf 3 | include test-requirements.txt 4 | recursive-include docs * 5 | prune docs/build 6 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | #!/usr/bin/make -f 2 | 3 | PACKAGE=asyncgpio 4 | ifneq ($(wildcard /usr/share/sourcemgr/make/py),) 5 | include /usr/share/sourcemgr/make/py 6 | # availabe via http://github.com/smurfix/sourcemgr 7 | 8 | else 9 | %: 10 | @echo "Please use 'python setup.py'." 11 | @exit 1 12 | endif 13 | 14 | pytest: 15 | sudo tests/test.sh 16 | 17 | .PHONY: modload 18 | 19 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | asyncgpio 2 | ========= 3 | 4 | AsyncGPIO allows easy access to the GPIO pins on your Raspberry Pi or 5 | similar embedded computer. 6 | 7 | It is based on libgpiod and its CFFI adapter by Steven P. Goldsmith 8 | , as downloaded from 9 | `github `_. 10 | 11 | To run examples, make sure to install `trio` first. 12 | 13 | Testing AsyncGPIO requires a Linux distribution that enables the mock-GPIO module. 14 | As of mid-2020, Debian's kernel does not include this module, but Raspbian's does. 15 | 16 | If you can compile your own kernel: the option is named CONFIG_GPIO_MOCKUP, 17 | in Device Drivers / GPIO support / Memory mapped GPIO drivers / GPIO 18 | Testing Driver. 19 | 20 | Writing an actual test suite is TODO. There is a more elaborate test script 21 | in `DistKV-GPIO `_. 22 | -------------------------------------------------------------------------------- /asyncgpio/__init__.py: -------------------------------------------------------------------------------- 1 | """Top-level package for asyncgpio.""" 2 | 3 | import sys 4 | 5 | from .gpio import Chip 6 | from .libgpiod import * # noqa 7 | 8 | 9 | def open_chip(num=None, label=None, consumer=sys.argv[0]): 10 | """Returns an object representing a GPIO chip. 11 | 12 | Arguments: 13 | num: Chip number. Defaults to zero. 14 | 15 | consumer: A string for display by kernel utilities. 16 | Defaults to the program's name. 17 | 18 | Returns: 19 | a :class:`asyncgpio.gpio.Chip` instance. 20 | """ 21 | return Chip(num=num, label=label, consumer=consumer) 22 | -------------------------------------------------------------------------------- /asyncgpio/gpio.py: -------------------------------------------------------------------------------- 1 | from . import libgpiod as gpio 2 | 3 | import sys 4 | import anyio 5 | import datetime 6 | 7 | 8 | class _sock: 9 | def __init__(self,fd): 10 | self.fd = fd 11 | def fileno(self): 12 | return self.fd 13 | 14 | class Chip: 15 | """Represents a GPIO chip. 16 | 17 | Arguments: 18 | label: Chip label. Run "gpiodetect" to list GPIO chip labels. 19 | 20 | num: Chip number. Deprecated. Defaults to zero. 21 | Only used if you don't use a label. 22 | 23 | consumer: A string for display by kernel utilities. 24 | Defaults to the program name. 25 | 26 | """ 27 | 28 | _chip = None 29 | 30 | def __init__(self, num=None, label=None, consumer=sys.argv[0]): 31 | self._num = num 32 | self._label = label 33 | if (num is None) == (label is None): 34 | raise ValueError("Specify either label or num") 35 | self._consumer = consumer 36 | 37 | def __repr__(self): 38 | if self._label is None: 39 | return "%s(%d)" % (self.__class__.__name__, self._num) 40 | else: 41 | return "%s(%s)" % (self.__class__.__name__, self._label) 42 | 43 | def __enter__(self): 44 | if self._label is None: 45 | self._chip = gpio.lib.gpiod_chip_open_by_number(self._num) 46 | else: 47 | self._chip = gpio.lib.gpiod_chip_open_by_label(self._label.encode("utf-8")) 48 | if self._chip == gpio.ffi.NULL: 49 | raise OSError("unable to open chip") 50 | return self 51 | 52 | def __exit__(self, *tb): 53 | gpio.lib.gpiod_chip_close(self._chip) 54 | self._chip = None 55 | 56 | def line(self, offset, consumer=None): 57 | """Get a descriptor for a single GPIO line. 58 | 59 | Arguments: 60 | offset: GPIO number within this chip. No default. 61 | consumer: override the chip's consumer, if required. 62 | """ 63 | if consumer is None: 64 | consumer = self._consumer 65 | return Line(self, offset, consumer=consumer) 66 | 67 | 68 | _FREE = 0 69 | _PRE_IO = 1 70 | _IN_IO = 2 71 | _PRE_EV = 3 72 | _IN_EV = 4 73 | _IN_USE = {_IN_IO, _IN_EV} 74 | 75 | 76 | class Line: 77 | """Represents a single GPIO line. 78 | 79 | Create this object by calling :meth:`Chip.line`. 80 | """ 81 | 82 | _line = None 83 | _direction = None 84 | _default = None 85 | _flags = None 86 | _ev_flags = None 87 | _state = _FREE 88 | 89 | _type = None 90 | 91 | def __init__(self, chip, offset, consumer=sys.argv[0][:-3]): 92 | self._chip = chip 93 | self._offset = offset 94 | self._consumer = consumer.encode("utf-8") 95 | self.__consumer = gpio.ffi.new("char[]", self._consumer) 96 | 97 | def __repr__(self): 98 | return "<%s %s:%d %s=%d>" % ( 99 | self.__class__.__name__, 100 | self._chip, 101 | self._offset, 102 | self._line, 103 | self._state, 104 | ) 105 | 106 | def open(self, direction=gpio.DIRECTION_INPUT, default=False, flags=0): 107 | """ 108 | Create a context manager for controlling this line's input or output. 109 | 110 | Arguments: 111 | direction: input or output. Default: gpio.DIRECTION_INPUT. 112 | flags: to request pull-up/down resistors or open-collector outputs. 113 | 114 | Example:: 115 | with gpio.Chip(0) as chip: 116 | line = chip.line(16) 117 | with line.open(direction=gpio.DIRECTION_INPUT) as wire: 118 | print(wire.value) 119 | """ 120 | if self._state in _IN_USE: 121 | raise OSError("This line is already in use") 122 | self._direction = direction 123 | self._default = default 124 | self._flags = flags 125 | self._state = _PRE_IO 126 | return self 127 | 128 | def __enter__(self): 129 | """Context management for use with :meth:`open` and :meth:`monitor`.""" 130 | if self._state in _IN_USE: 131 | raise OSError("This line is already in use") 132 | if self._state == _FREE: 133 | raise RuntimeError("You need to call .open() or .monitor()") 134 | self._line = gpio.lib.gpiod_chip_get_line(self._chip._chip, self._offset) 135 | if self._line == gpio.ffi.NULL: 136 | raise OSError("unable to get line") 137 | 138 | if self._state == _PRE_IO: 139 | self._enter_io() 140 | elif self._state == _PRE_EV: 141 | self._enter_ev() 142 | else: 143 | raise RuntimeError("wrong state", self) 144 | return self 145 | 146 | def _enter_io(self): 147 | if self._direction == gpio.DIRECTION_INPUT: 148 | r = gpio.lib.gpiod_line_request_input_flags(self._line, self._consumer, self._flags) 149 | elif self._direction == gpio.DIRECTION_OUTPUT: 150 | r = gpio.lib.gpiod_line_request_output_flags( 151 | self._line, self._consumer, self._flags, self._default 152 | ) 153 | else: 154 | self.__exit__() 155 | raise RuntimeError("Unknown direction") 156 | if r != 0: 157 | self.__exit__() 158 | raise OSError("unable to set direction") 159 | self._state = _IN_IO 160 | return self 161 | 162 | def _enter_ev(self): 163 | req = gpio.ffi.new("struct gpiod_line_request_config*") 164 | req.consumer = self.__consumer 165 | req.request_type = self._type 166 | req.flags = self._flags 167 | if gpio.lib.gpiod_line_request(self._line, req, 0) != 0: 168 | raise OSError("unable to request event monitoring") 169 | self._state = _IN_EV 170 | 171 | def __exit__(self, *tb): 172 | if self._line is not None: 173 | try: 174 | gpio.lib.gpiod_line_release(self._line) 175 | finally: 176 | self._line = None 177 | self._state = _FREE 178 | 179 | def _is_open(self): 180 | if self._state not in _IN_USE: 181 | raise RuntimeError("Line is not open", self) 182 | 183 | @property 184 | def value(self): 185 | self._is_open() 186 | return gpio.lib.gpiod_line_get_value(self._line) 187 | 188 | @value.setter 189 | def value(self, value): 190 | self._is_open() 191 | gpio.lib.gpiod_line_set_value(self._line, value) 192 | 193 | @property 194 | def direction(self): 195 | if self._line is None: 196 | return self._direction 197 | return gpio.lib.gpiod_line_direction(self._line) 198 | 199 | @property 200 | def active_state(self): 201 | self._is_open() 202 | return gpio.lib.gpiod_line_active_state(self._line) 203 | 204 | @property 205 | def is_open_drain(self): 206 | self._is_open() 207 | return gpio.lib.gpiod_line_is_open_drain(self._line) 208 | 209 | @property 210 | def is_open_source(self): 211 | self._is_open() 212 | return gpio.lib.gpiod_line_is_open_source(self._line) 213 | 214 | @property 215 | def is_used(self): 216 | self._is_open() 217 | return gpio.lib.gpiod_line_is_used(self._line) 218 | 219 | @property 220 | def offset(self): 221 | if self._line is None: 222 | return self._offset 223 | return gpio.lib.gpiod_line_offset(self._line) 224 | 225 | @property 226 | def name(self): 227 | self._is_open() 228 | n = gpio.lib.gpiod_line_name(self._line) 229 | if n == gpio.ffi.NULL: 230 | return None 231 | return n 232 | 233 | @property 234 | def consumer(self): 235 | if self._line is None: 236 | return self._consumer 237 | n = gpio.lib.gpiod_line_consumer(self._line) 238 | if n == gpio.ffi.NULL: 239 | return None 240 | return gpio.ffi.string(n).decode("utf-8") 241 | 242 | def monitor( 243 | self, type=gpio.REQUEST_EVENT_RISING_EDGE, flags=0 244 | ): # pylint: disable=redefined-builtin 245 | """ 246 | Monitor events. 247 | 248 | Arguments: 249 | type: which edge to monitor 250 | flags: REQUEST_FLAG_* values (ORed) 251 | 252 | Usage:: 253 | 254 | with gpio.Chip(0) as chip: 255 | line = chip.line(13) 256 | with line.monitor(): 257 | async for event in line: 258 | print(event) 259 | """ 260 | if self._state in _IN_USE: 261 | raise OSError("This line is already in use") 262 | self._state = _PRE_EV 263 | self._type = type 264 | self._flags = flags 265 | return self 266 | 267 | def _update(self): 268 | self._is_open() 269 | if gpio.lib.gpiod_line_update(self._line) == -1: 270 | raise OSError("unable to update state") 271 | 272 | def __iter__(self): 273 | raise RuntimeError("You need to use 'async for', not 'for'") 274 | 275 | async def __aenter__(self): 276 | raise RuntimeError("You need to use 'with', not 'async with'") 277 | 278 | async def __aexit__(self, *_): 279 | raise RuntimeError("You need to use 'with', not 'async with'") 280 | 281 | def __aiter__(self): 282 | if self._state != _IN_EV: 283 | raise RuntimeError("You need to call 'with LINE.monitor() / async for event in LINE'") 284 | return self 285 | 286 | async def __anext__(self): 287 | if self._state != _IN_EV: 288 | raise RuntimeError("wrong state") 289 | 290 | ev = gpio.ffi.new("struct gpiod_line_event*") 291 | fd = gpio.lib.gpiod_line_event_get_fd(self._line) 292 | if fd < 0: 293 | raise OSError("line is closed") 294 | await anyio.wait_socket_readable(_sock(fd)) 295 | self._is_open() 296 | r = gpio.lib.gpiod_line_event_read_fd(fd, ev) 297 | if r != 0: 298 | raise OSError("unable to read update") 299 | return Event(ev) 300 | 301 | async def aclose(self): 302 | """close the iterator.""" 303 | pass 304 | 305 | 306 | class Event: 307 | """Store a Pythonic representation of an event 308 | """ 309 | 310 | def __init__(self, ev): 311 | if ev.event_type == gpio.EVENT_RISING_EDGE: 312 | self.value = 1 313 | elif ev.event_type == gpio.EVENT_FALLING_EDGE: 314 | self.value = 0 315 | else: 316 | raise RuntimeError("Unknown event type") 317 | self._ts_sec = ev.ts.tv_sec 318 | self._ts_nsec = ev.ts.tv_nsec 319 | 320 | @property 321 | def timestamp(self): 322 | """Return a (second,nanosecond) tuple for fast timestamping""" 323 | return (self._ts_sec, self._ts_nsec) 324 | 325 | @property 326 | def time(self): 327 | """Return the event's proper datetime""" 328 | return datetime.datetime.fromtimestamp(self._ts_sec + self._ts_nsec / 1000000000) 329 | 330 | def __repr__(self): 331 | return "<%s @%s>" % (self.value, self.time) 332 | -------------------------------------------------------------------------------- /asyncgpio/libgpiod.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Copyright (c) 2018 Matthias Urlichs 4 | # Based on work Copyright (c) 2018 Steven P. Goldsmith 5 | """ 6 | libgpiod CFFI interface 7 | ------------- 8 | 9 | This is a stripped-down version which doesn't do "bulk" access (no point IMHO) 10 | and doesn't implement an event loop (that's Trio's job). 11 | """ 12 | 13 | from cffi import FFI 14 | 15 | __all__ = [ 16 | "DIRECTION_INPUT", 17 | "DIRECTION_OUTPUT", 18 | "ACTIVE_STATE_HIGH", 19 | "ACTIVE_STATE_LOW", 20 | "REQUEST_DIRECTION_AS_IS", 21 | "REQUEST_DIRECTION_INPUT", 22 | "REQUEST_DIRECTION_OUTPUT", 23 | "REQUEST_EVENT_FALLING_EDGE", 24 | "REQUEST_EVENT_RISING_EDGE", 25 | "REQUEST_EVENT_BOTH_EDGES", 26 | "REQUEST_FLAG_OPEN_DRAIN", 27 | "REQUEST_FLAG_OPEN_SOURCE", 28 | "REQUEST_FLAG_ACTIVE_LOW", 29 | "EVENT_RISING_EDGE", 30 | "EVENT_FALLING_EDGE", 31 | "ffi", 32 | "lib", 33 | ] 34 | 35 | ffi = FFI() 36 | ffi.cdef( 37 | """ 38 | enum { 39 | GPIOD_CTXLESS_EVENT_CB_TIMEOUT = 1, 40 | GPIOD_CTXLESS_EVENT_CB_RISING_EDGE, 41 | GPIOD_CTXLESS_EVENT_CB_FALLING_EDGE, 42 | }; 43 | 44 | enum { 45 | GPIOD_CTXLESS_EVENT_CB_RET_ERR = -1, 46 | GPIOD_CTXLESS_EVENT_CB_RET_OK = 0, 47 | GPIOD_CTXLESS_EVENT_CB_RET_STOP = 1, 48 | }; 49 | 50 | enum { 51 | GPIOD_CTXLESS_EVENT_POLL_RET_STOP = -2, 52 | GPIOD_CTXLESS_EVENT_POLL_RET_ERR = -1, 53 | GPIOD_CTXLESS_EVENT_POLL_RET_TIMEOUT = 0, 54 | }; 55 | 56 | enum { 57 | GPIOD_LINE_DIRECTION_INPUT = 1, 58 | GPIOD_LINE_DIRECTION_OUTPUT, 59 | }; 60 | 61 | enum { 62 | GPIOD_LINE_ACTIVE_STATE_HIGH = 1, 63 | GPIOD_LINE_ACTIVE_STATE_LOW, 64 | }; 65 | 66 | enum { 67 | GPIOD_LINE_REQUEST_DIRECTION_AS_IS = 1, 68 | GPIOD_LINE_REQUEST_DIRECTION_INPUT, 69 | GPIOD_LINE_REQUEST_DIRECTION_OUTPUT, 70 | GPIOD_LINE_REQUEST_EVENT_FALLING_EDGE, 71 | GPIOD_LINE_REQUEST_EVENT_RISING_EDGE, 72 | GPIOD_LINE_REQUEST_EVENT_BOTH_EDGES, 73 | }; 74 | 75 | enum { 76 | GPIOD_LINE_REQUEST_FLAG_OPEN_DRAIN = 1, 77 | GPIOD_LINE_REQUEST_FLAG_OPEN_SOURCE = 2, 78 | GPIOD_LINE_REQUEST_FLAG_ACTIVE_LOW = 4, 79 | GPIOD_LINE_REQUEST_FLAG_BIAS_DISABLE = 8, 80 | GPIOD_LINE_REQUEST_FLAG_BIAS_PULL_DOWN = 16, 81 | GPIOD_LINE_REQUEST_FLAG_BIAS_PULL_UP = 32, 82 | }; 83 | 84 | enum { 85 | GPIOD_LINE_EVENT_RISING_EDGE = 1, 86 | GPIOD_LINE_EVENT_FALLING_EDGE, 87 | }; 88 | 89 | struct timespec { 90 | long tv_sec; 91 | long tv_nsec; 92 | }; 93 | 94 | struct gpiod_line { 95 | unsigned int offset; 96 | int direction; 97 | int active_state; 98 | bool used; 99 | bool open_source; 100 | bool open_drain; 101 | int state; 102 | bool up_to_date; 103 | struct gpiod_chip *chip; 104 | int fd; 105 | char name[32]; 106 | char consumer[32]; 107 | }; 108 | 109 | struct gpiod_chip { 110 | struct gpiod_line **lines; 111 | unsigned int num_lines; 112 | int fd; 113 | char name[32]; 114 | char label[32]; 115 | }; 116 | 117 | struct gpiod_ctxless_event_poll_fd { 118 | int fd; 119 | /**< File descriptor number. */ 120 | bool event; 121 | /**< Indicates whether an event occurred on this file descriptor. */ 122 | }; 123 | 124 | struct gpiod_line_request_config { 125 | const char *consumer; 126 | int request_type; 127 | int flags; 128 | }; 129 | 130 | struct gpiod_line_event { 131 | struct timespec ts; 132 | int event_type; 133 | }; 134 | 135 | struct gpiod_chip; 136 | 137 | struct gpiod_line; 138 | 139 | struct gpiod_chip_iter; 140 | 141 | struct gpiod_line_iter; 142 | 143 | struct gpiod_line_bulk; 144 | 145 | typedef void (*gpiod_ctxless_set_value_cb)(void *); 146 | 147 | typedef int (*gpiod_ctxless_event_handle_cb)(int, unsigned int, 148 | const struct timespec *, void *); 149 | 150 | typedef int (*gpiod_ctxless_event_poll_cb)(unsigned int, 151 | struct gpiod_ctxless_event_poll_fd *, 152 | const struct timespec *, void *); 153 | 154 | int gpiod_ctxless_set_value(const char *device, unsigned int offset, int value, 155 | bool active_low, const char *consumer, 156 | gpiod_ctxless_set_value_cb cb, 157 | void *data); 158 | 159 | int gpiod_ctxless_set_value_multiple(const char *device, 160 | const unsigned int *offsets, 161 | const int *values, unsigned int num_lines, 162 | bool active_low, const char *consumer, 163 | gpiod_ctxless_set_value_cb cb, 164 | void *data); 165 | 166 | int gpiod_ctxless_find_line(const char *name, char *chipname, 167 | size_t chipname_size, 168 | unsigned int *offset); 169 | 170 | int gpiod_chip_find_lines(struct gpiod_chip *chip, const char **names, 171 | struct gpiod_line_bulk *bulk); 172 | 173 | struct gpiod_chip *gpiod_chip_open(const char *path); 174 | 175 | struct gpiod_chip *gpiod_chip_open_by_name(const char *name); 176 | 177 | struct gpiod_chip *gpiod_chip_open_by_number(unsigned int num); 178 | 179 | struct gpiod_chip *gpiod_chip_open_by_label(const char *label); 180 | 181 | struct gpiod_chip *gpiod_chip_open_lookup(const char *descr); 182 | 183 | void gpiod_chip_close(struct gpiod_chip *chip); 184 | 185 | const char *gpiod_chip_name(struct gpiod_chip *chip); 186 | 187 | const char *gpiod_chip_label(struct gpiod_chip *chip); 188 | 189 | unsigned int gpiod_chip_num_lines(struct gpiod_chip *chip); 190 | 191 | struct gpiod_line * 192 | gpiod_chip_get_line(struct gpiod_chip *chip, unsigned int offset); 193 | 194 | int gpiod_chip_get_lines(struct gpiod_chip *chip, 195 | unsigned int *offsets, unsigned int num_offsets, 196 | struct gpiod_line_bulk *bulk); 197 | 198 | int gpiod_chip_get_all_lines(struct gpiod_chip *chip, 199 | struct gpiod_line_bulk *bulk); 200 | 201 | struct gpiod_line * 202 | gpiod_chip_find_line(struct gpiod_chip *chip, const char *name); 203 | 204 | unsigned int gpiod_line_offset(struct gpiod_line *line); 205 | 206 | const char *gpiod_line_name(struct gpiod_line *line); 207 | 208 | const char *gpiod_line_consumer(struct gpiod_line *line); 209 | 210 | int gpiod_line_direction(struct gpiod_line *line); 211 | 212 | int gpiod_line_active_state(struct gpiod_line *line); 213 | 214 | bool gpiod_line_is_used(struct gpiod_line *line); 215 | 216 | bool gpiod_line_is_open_drain(struct gpiod_line *line); 217 | 218 | bool gpiod_line_is_open_source(struct gpiod_line *line); 219 | 220 | int gpiod_line_update(struct gpiod_line *line); 221 | 222 | bool gpiod_line_needs_update(struct gpiod_line *line); 223 | 224 | int gpiod_line_request(struct gpiod_line *line, 225 | const struct gpiod_line_request_config *config, 226 | int default_val); 227 | 228 | int gpiod_line_request_input(struct gpiod_line *line, 229 | const char *consumer); 230 | 231 | int gpiod_line_request_output(struct gpiod_line *line, 232 | const char *consumer, int default_val); 233 | 234 | int gpiod_line_request_rising_edge_events(struct gpiod_line *line, 235 | const char *consumer); 236 | 237 | int gpiod_line_request_falling_edge_events(struct gpiod_line *line, 238 | const char *consumer); 239 | 240 | int gpiod_line_request_both_edges_events(struct gpiod_line *line, 241 | const char *consumer); 242 | 243 | int gpiod_line_request_input_flags(struct gpiod_line *line, 244 | const char *consumer, int flags); 245 | 246 | int gpiod_line_request_output_flags(struct gpiod_line *line, 247 | const char *consumer, int flags, 248 | int default_val); 249 | 250 | int gpiod_line_request_rising_edge_events_flags(struct gpiod_line *line, 251 | const char *consumer, 252 | int flags); 253 | 254 | int gpiod_line_request_falling_edge_events_flags(struct gpiod_line *line, 255 | const char *consumer, 256 | int flags); 257 | 258 | int gpiod_line_request_both_edges_events_flags(struct gpiod_line *line, 259 | const char *consumer, 260 | int flags); 261 | 262 | void gpiod_line_release(struct gpiod_line *line); 263 | 264 | bool gpiod_line_is_requested(struct gpiod_line *line); 265 | 266 | bool gpiod_line_is_free(struct gpiod_line *line); 267 | 268 | int gpiod_line_get_value(struct gpiod_line *line); 269 | 270 | int gpiod_line_set_value(struct gpiod_line *line, int value); 271 | 272 | int gpiod_line_set_value_bulk(struct gpiod_line_bulk *bulk, 273 | const int *values); 274 | 275 | int gpiod_line_event_wait(struct gpiod_line *line, 276 | const struct timespec *timeout); 277 | 278 | int gpiod_line_event_read(struct gpiod_line *line, 279 | struct gpiod_line_event *event); 280 | 281 | int gpiod_line_event_get_fd(struct gpiod_line *line); 282 | 283 | int gpiod_line_event_read_fd(int fd, struct gpiod_line_event *event); 284 | 285 | struct gpiod_line * 286 | gpiod_line_get(const char *device, unsigned int offset); 287 | 288 | struct gpiod_line *gpiod_line_find(const char *name); 289 | 290 | void gpiod_line_close_chip(struct gpiod_line *line); 291 | 292 | struct gpiod_chip *gpiod_line_get_chip(struct gpiod_line *line); 293 | 294 | struct gpiod_chip_iter *gpiod_chip_iter_new(void); 295 | 296 | void gpiod_chip_iter_free(struct gpiod_chip_iter *iter); 297 | 298 | void gpiod_chip_iter_free_noclose(struct gpiod_chip_iter *iter); 299 | 300 | struct gpiod_chip * 301 | gpiod_chip_iter_next(struct gpiod_chip_iter *iter); 302 | 303 | struct gpiod_chip * 304 | gpiod_chip_iter_next_noclose(struct gpiod_chip_iter *iter); 305 | 306 | struct gpiod_line_iter * 307 | gpiod_line_iter_new(struct gpiod_chip *chip); 308 | 309 | void gpiod_line_iter_free(struct gpiod_line_iter *iter); 310 | 311 | struct gpiod_line * 312 | gpiod_line_iter_next(struct gpiod_line_iter *iter); 313 | 314 | const char *gpiod_version_string(void); 315 | """ 316 | ) 317 | 318 | try: 319 | lib = ffi.dlopen("libgpiod.so.2") 320 | except OSError: 321 | lib = ffi.dlopen("c") # workaround if we're only building docs 322 | 323 | DIRECTION_INPUT = lib.GPIOD_LINE_REQUEST_DIRECTION_INPUT 324 | DIRECTION_OUTPUT = lib.GPIOD_LINE_REQUEST_DIRECTION_OUTPUT 325 | ACTIVE_STATE_HIGH = lib.GPIOD_LINE_ACTIVE_STATE_HIGH 326 | ACTIVE_STATE_LOW = lib.GPIOD_LINE_ACTIVE_STATE_LOW 327 | REQUEST_DIRECTION_AS_IS = lib.GPIOD_LINE_REQUEST_DIRECTION_AS_IS 328 | REQUEST_DIRECTION_INPUT = lib.GPIOD_LINE_REQUEST_DIRECTION_INPUT 329 | REQUEST_DIRECTION_OUTPUT = lib.GPIOD_LINE_REQUEST_DIRECTION_OUTPUT 330 | REQUEST_EVENT_FALLING_EDGE = lib.GPIOD_LINE_REQUEST_EVENT_FALLING_EDGE 331 | REQUEST_EVENT_RISING_EDGE = lib.GPIOD_LINE_REQUEST_EVENT_RISING_EDGE 332 | REQUEST_EVENT_BOTH_EDGES = lib.GPIOD_LINE_REQUEST_EVENT_BOTH_EDGES 333 | 334 | REQUEST_FLAG_OPEN_DRAIN = lib.GPIOD_LINE_REQUEST_FLAG_OPEN_DRAIN 335 | REQUEST_FLAG_OPEN_SOURCE = lib.GPIOD_LINE_REQUEST_FLAG_OPEN_SOURCE 336 | REQUEST_FLAG_ACTIVE_LOW = lib.GPIOD_LINE_REQUEST_FLAG_ACTIVE_LOW 337 | EVENT_RISING_EDGE = lib.GPIOD_LINE_EVENT_RISING_EDGE 338 | EVENT_FALLING_EDGE = lib.GPIOD_LINE_EVENT_FALLING_EDGE 339 | -------------------------------------------------------------------------------- /asyncgpio/test.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module contains helpers for testing async gpio, via the Linux kernel's 3 | ``gpio_mockup`` module (writing) and ``/sys/kernel/debug/cpio`` (monitoring). 4 | """ 5 | 6 | import os 7 | import re 8 | import anyio 9 | import logging 10 | import errno 11 | 12 | from contextlib import asynccontextmanager 13 | from collections import namedtuple 14 | 15 | logger = logging.getLogger(__name__) 16 | 17 | _r_chip = re.compile( 18 | "^(?P[a-z0-9]+): GPIOs (?P[0-9]+)-(?:.*, (?P[-_a-zA-Z0-9]+): *$)?" 19 | ) 20 | _r_pin = re.compile("^gpio-(?P[0-9]+) \\(.*\\) (?Pin|out) +(?Phi|lo)") 21 | 22 | Pin = namedtuple("Pin", ["out", "level"]) 23 | 24 | 25 | class _GpioPin: 26 | """ 27 | Code representing one GPIO pin. 28 | """ 29 | 30 | fd = None 31 | 32 | def __init__(self, watcher: "GpioWatcher", chip: str, pin: int): 33 | self.watcher = watcher 34 | self.chip = chip 35 | self.pin = pin 36 | self.mon = set() 37 | self.state = (None, None) 38 | try: 39 | self.fd = os.open( 40 | os.path.join(watcher.debugfs_path, "gpio-mockup-event", chip, str(pin)), 41 | os.O_WRONLY, 42 | ) 43 | except EnvironmentError as exc: 44 | if exc.errno != errno.ENOENT: 45 | raise 46 | 47 | def __del__(self): 48 | if self.fd is not None: 49 | os.close(self.fd) 50 | del self.fd 51 | 52 | @asynccontextmanager 53 | async def watch(self): 54 | """ 55 | An async context manager that returns an iterator for changes of 56 | this pin. 57 | 58 | Values are (out,level) tuples of bool, with "out" and "high" 59 | represented as True. 60 | """ 61 | q = anyio.create_queue(10) 62 | self.mon.add(q) 63 | try: 64 | yield q 65 | finally: 66 | self.mon.remove(q) 67 | 68 | async def see(self, write: bool, level: bool): 69 | s = (write, level) 70 | if self.state == s: 71 | return 72 | self.state = s 73 | logger.debug("SEE %s %d %s", self.chip, self.pin, self.state) 74 | for cb in list(self.mon): 75 | await cb.put(s) 76 | 77 | def set(self, value: bool): 78 | logger.debug("SET %s %d %s", self.chip, self.pin, value) 79 | if self.fd is None: 80 | raise RuntimeError( 81 | "Pin %s/%d is not controlled via the 'gpio_mockup' module" % (self.chip, self.pin) 82 | ) 83 | os.write(self.fd, b"1" if value else b"0") 84 | # os.lseek(self.fd, 0, os.SEEK_SET) 85 | 86 | 87 | class GpioWatcher: 88 | """ 89 | Code which triggers callbacks whenever a GPIO pin changes. 90 | 91 | This class polls `/sys/kernel/debug/gpio` (can be overridden). 92 | """ 93 | 94 | tg = None # for .run 95 | 96 | def __init__( 97 | self, 98 | interval: float = 0.2, 99 | debugfs_path: str = "/sys/kernel/debug", 100 | sysfs_path: str = "/sys", 101 | ): 102 | self.interval = interval 103 | self.gpio = open(os.path.join(debugfs_path, "gpio"), "r") 104 | self.targets = dict() # chip > line > _GpioPin 105 | # self.names = {} 106 | self.sysfs_path = sysfs_path 107 | self.debugfs_path = debugfs_path 108 | # gpio_dir = os.path.join(sysfs_path, "class", "gpio") 109 | 110 | # for d in os.listdir(gpio_dir): 111 | # try: 112 | # with open(os.path.join(gpio_dir,d,"label"),"r") as f: 113 | # n = f.read().strip() 114 | # except EnvironmentError as e: 115 | # if e.errno == errno.ENOTDIR: 116 | # continue 117 | # raise 118 | # else: 119 | # self.names[d] = n 120 | 121 | def monitor(self, chip: str, pin: int): 122 | """ 123 | Shortcut for 'self.pin(chip, pin).watch()'. 124 | """ 125 | return self.pin(chip, pin).watch() 126 | 127 | def pin(self, chip: str, pin: int, create: bool = True): 128 | """ 129 | Returns a pins corresponding GpioPin 130 | """ 131 | # chip = self.names[chip] 132 | try: 133 | c = self.targets[chip] 134 | except KeyError: 135 | if not create: 136 | raise 137 | self.targets[chip] = c = dict() 138 | try: 139 | p = c[pin] 140 | except KeyError: 141 | if not create: 142 | raise 143 | c[pin] = p = _GpioPin(self, chip, pin) 144 | return p 145 | 146 | async def _watch(self): 147 | # The actual monitor. 148 | while True: 149 | await self.check_pins() 150 | await anyio.sleep(self.interval) 151 | 152 | async def check_pins(self): 153 | """ 154 | Read the GPIO debug file and update pin states 155 | """ 156 | chip = None 157 | base = None 158 | 159 | for line in self.gpio: 160 | line = line.strip() 161 | if not line: 162 | chip = None 163 | continue 164 | if chip is None: 165 | r = _r_chip.match(line) 166 | if not r: 167 | raise ValueError(line) 168 | chip = r.group("name") 169 | if not chip: 170 | chip = r.group("chip") 171 | base = int(r.group("base")) 172 | else: 173 | r = _r_pin.match(line) 174 | if not r: 175 | breakpoint() 176 | raise ValueError(line) 177 | pin = int(r.group("pin")) - base 178 | out = r.group("dir") == "out" 179 | val = r.group("val") == "hi" 180 | 181 | try: 182 | pin = self.pin(chip, pin, create=False) 183 | except KeyError: 184 | pass 185 | else: 186 | await pin.see(out, val) 187 | self.gpio.seek(0) 188 | 189 | @asynccontextmanager 190 | async def run(self): 191 | """ 192 | This async context manager controls the monitoring loop. 193 | """ 194 | async with anyio.create_task_group() as tg: 195 | self.tg = tg 196 | await tg.spawn(self._watch) 197 | try: 198 | yield self 199 | finally: 200 | self.tg = None 201 | await tg.cancel_scope.cancel() 202 | -------------------------------------------------------------------------------- /ci/rtd-requirements.txt: -------------------------------------------------------------------------------- 1 | # RTD is currently installing 1.5.3, which has a bug in :lineno-match: 2 | sphinx >= 1.7.0 3 | sphinx_rtd_theme 4 | sphinxcontrib-trio 5 | -------------------------------------------------------------------------------- /ci/test-requirements.txt: -------------------------------------------------------------------------------- 1 | pytest-trio 2 | pytest-cov 3 | -------------------------------------------------------------------------------- /ci/travis.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -ex 4 | 5 | # See https://github.com/python-trio/trio/issues/334 6 | YAPF_VERSION=0.17.0 7 | 8 | pip install -U pip setuptools wheel 9 | 10 | if [ "$CHECK_FORMATTING" = "1" ]; then 11 | pip install yapf==${YAPF_VERSION} 12 | if ! yapf -rpd setup.py asyncgpio; then 13 | cat <NUL 2>NUL 17 | if errorlevel 9009 ( 18 | echo. 19 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 20 | echo.installed, then set the SPHINXBUILD environment variable to point 21 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 22 | echo.may add the Sphinx directory to PATH. 23 | echo. 24 | echo.If you don't have Sphinx installed, grab it from 25 | echo.http://sphinx-doc.org/ 26 | exit /b 1 27 | ) 28 | 29 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 30 | goto end 31 | 32 | :help 33 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 34 | 35 | :end 36 | popd 37 | -------------------------------------------------------------------------------- /docs/source/_static/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python-trio/asyncgpio/2761baec88f9970f814ccdbca24c0bc4e363916a/docs/source/_static/.gitkeep -------------------------------------------------------------------------------- /docs/source/conf.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # 4 | # Documentation build configuration file, created by 5 | # sphinx-quickstart on Sat Jan 21 19:11:14 2017. 6 | # 7 | # This file is execfile()d with the current directory set to its 8 | # containing dir. 9 | # 10 | # Note that not all possible configuration values are present in this 11 | # autogenerated file. 12 | # 13 | # All configuration values have a default; values that are commented out 14 | # serve to show the default. 15 | 16 | # If extensions (or modules to document with autodoc) are in another directory, 17 | # add these directories to sys.path here. If the directory is relative to the 18 | # documentation root, use os.path.abspath to make it absolute, like shown here. 19 | # 20 | import os 21 | import sys 22 | # So autodoc can import our package 23 | sys.path.insert(0, os.path.abspath('../..')) 24 | 25 | # Warn about all references to unknown targets 26 | nitpicky = True 27 | # Except for these ones, which we expect to point to unknown targets: 28 | nitpick_ignore = [ 29 | # Format is ("sphinx reference type", "string"), e.g.: 30 | ("py:obj", "bytes-like"), 31 | ("py:class", "asyncgpio.gpio.Chip"), 32 | ("py:class", "asyncgpio.gpio.Line"), 33 | ] 34 | 35 | autodoc_inherit_docstrings = False 36 | 37 | # -- General configuration ------------------------------------------------ 38 | 39 | # If your documentation needs a minimal Sphinx version, state it here. 40 | # 41 | # needs_sphinx = '1.0' 42 | 43 | # Add any Sphinx extension module names here, as strings. They can be 44 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 45 | # ones. 46 | extensions = [ 47 | 'sphinx.ext.autodoc', 48 | 'sphinx.ext.intersphinx', 49 | 'sphinx.ext.coverage', 50 | 'sphinx.ext.napoleon', 51 | 'sphinxcontrib_trio', 52 | ] 53 | 54 | intersphinx_mapping = { 55 | "python": ('https://docs.python.org/3', None), 56 | "trio": ('https://trio.readthedocs.io/en/stable', None), 57 | } 58 | 59 | autodoc_member_order = "bysource" 60 | 61 | # Add any paths that contain templates here, relative to this directory. 62 | templates_path = [] 63 | 64 | # The suffix(es) of source filenames. 65 | # You can specify multiple suffix as a list of string: 66 | # 67 | # source_suffix = ['.rst', '.md'] 68 | source_suffix = '.rst' 69 | 70 | # The master toctree document. 71 | master_doc = 'index' 72 | 73 | # General information about the project. 74 | project = 'asyncgpio' 75 | copyright = 'The asyncgpio authors' 76 | author = 'The asyncgpio authors' 77 | 78 | # The version info for the project you're documenting, acts as replacement for 79 | # |version| and |release|, also used in various other places throughout the 80 | # built documents. 81 | # 82 | # The short X.Y version. 83 | import asyncgpio 84 | version = asyncgpio.__version__ 85 | # The full version, including alpha/beta/rc tags. 86 | release = version 87 | 88 | # The language for content autogenerated by Sphinx. Refer to documentation 89 | # for a list of supported languages. 90 | # 91 | # This is also used if you do content translation via gettext catalogs. 92 | # Usually you set "language" from the command line for these cases. 93 | language = None 94 | 95 | # List of patterns, relative to source directory, that match files and 96 | # directories to ignore when looking for source files. 97 | # This patterns also effect to html_static_path and html_extra_path 98 | exclude_patterns = [] 99 | 100 | # The name of the Pygments (syntax highlighting) style to use. 101 | pygments_style = 'sphinx' 102 | 103 | # The default language for :: blocks 104 | highlight_language = 'python3' 105 | 106 | # If true, `todo` and `todoList` produce output, else they produce nothing. 107 | todo_include_todos = False 108 | 109 | 110 | # -- Options for HTML output ---------------------------------------------- 111 | 112 | # The theme to use for HTML and HTML Help pages. See the documentation for 113 | # a list of builtin themes. 114 | # 115 | #html_theme = 'alabaster' 116 | 117 | # We have to set this ourselves, not only because it's useful for local 118 | # testing, but also because if we don't then RTD will throw away our 119 | # html_theme_options. 120 | import sphinx_rtd_theme 121 | html_theme = 'sphinx_rtd_theme' 122 | html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] 123 | 124 | # Theme options are theme-specific and customize the look and feel of a theme 125 | # further. For a list of options available for each theme, see the 126 | # documentation. 127 | # 128 | html_theme_options = { 129 | # default is 2 130 | # show deeper nesting in the RTD theme's sidebar TOC 131 | # https://stackoverflow.com/questions/27669376/ 132 | # I'm not 100% sure this actually does anything with our current 133 | # versions/settings... 134 | "navigation_depth": 4, 135 | "logo_only": True, 136 | } 137 | 138 | # Add any paths that contain custom static files (such as style sheets) here, 139 | # relative to this directory. They are copied after the builtin static files, 140 | # so a file named "default.css" will overwrite the builtin "default.css". 141 | html_static_path = ['_static'] 142 | 143 | 144 | # -- Options for HTMLHelp output ------------------------------------------ 145 | 146 | # Output file base name for HTML help builder. 147 | htmlhelp_basename = 'asyncgpiodoc' 148 | 149 | 150 | # -- Options for LaTeX output --------------------------------------------- 151 | 152 | latex_elements = { 153 | # The paper size ('letterpaper' or 'a4paper'). 154 | # 155 | # 'papersize': 'letterpaper', 156 | 157 | # The font size ('10pt', '11pt' or '12pt'). 158 | # 159 | # 'pointsize': '10pt', 160 | 161 | # Additional stuff for the LaTeX preamble. 162 | # 163 | # 'preamble': '', 164 | 165 | # Latex figure (float) alignment 166 | # 167 | # 'figure_align': 'htbp', 168 | } 169 | 170 | # Grouping the document tree into LaTeX files. List of tuples 171 | # (source start file, target name, title, 172 | # author, documentclass [howto, manual, or own class]). 173 | latex_documents = [ 174 | (master_doc, 'asyncgpio.tex', 'Trio Documentation', 175 | author, 'manual'), 176 | ] 177 | 178 | 179 | # -- Options for manual page output --------------------------------------- 180 | 181 | # One entry per manual page. List of tuples 182 | # (source start file, name, description, authors, manual section). 183 | man_pages = [ 184 | (master_doc, 'asyncgpio', 'asyncgpio Documentation', 185 | [author], 1) 186 | ] 187 | 188 | 189 | # -- Options for Texinfo output ------------------------------------------- 190 | 191 | # Grouping the document tree into Texinfo files. List of tuples 192 | # (source start file, target name, title, author, 193 | # dir menu entry, description, category) 194 | texinfo_documents = [ 195 | (master_doc, 'asyncgpio', 'asyncgpio Documentation', 196 | author, 'asyncgpio', 'GPIO access via Trio and libgpiod', 197 | 'Miscellaneous'), 198 | ] 199 | -------------------------------------------------------------------------------- /docs/source/history.rst: -------------------------------------------------------------------------------- 1 | Release history 2 | =============== 3 | 4 | .. currentmodule:: asyncgpio 5 | 6 | .. towncrier release notes start 7 | -------------------------------------------------------------------------------- /docs/source/index.rst: -------------------------------------------------------------------------------- 1 | .. documentation master file, created by 2 | sphinx-quickstart on Sat Jan 21 19:11:14 2017. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | 7 | ============================================ 8 | asyncgpio: GPIO access via Trio and libgpiod 9 | ============================================ 10 | 11 | Trio-GPIO is a simple wrapper around ``libgpiod``. 12 | 13 | You can use Trio-GPIO to 14 | * access a GPIO chip 15 | * get an object describing a GPIO line 16 | * open the line for input or output 17 | * monitor the line for events (without polling!) 18 | 19 | Trio-GPIO only supports Linux. 20 | It uses the "new" GPIO interface, i.e. kernel 4.5 or later is required. 21 | 22 | .. toctree:: 23 | :maxdepth: 2 24 | 25 | usage.rst 26 | history.rst 27 | 28 | ==================== 29 | Indices and tables 30 | ==================== 31 | 32 | * :ref:`genindex` 33 | * :ref:`modindex` 34 | * :ref:`search` 35 | * :ref:`glossary` 36 | -------------------------------------------------------------------------------- /docs/source/usage.rst: -------------------------------------------------------------------------------- 1 | 2 | Using AsyncGPIO 3 | =============== 4 | 5 | .. module: asyncgpio 6 | 7 | Using AsyncGPIO generally consists of three steps: 8 | Accessing the chip, referring to a GPIO line within that chip. 9 | and actually using the line for input, output, or monitoring. 10 | 11 | AsyncGPIO currently only works with anyio's Trio backend. 12 | 13 | Accessing a GPIO chip 14 | --------------------- 15 | 16 | GPIO chips are accessed by their number. You usually find them as ``/dev/gpiochipN`` 17 | where N starts at zero. 18 | 19 | AsyncGPIO refers to GPIO chips by their sequence number. 20 | You need to refer to your hardware's documentation to discover which chip to use, 21 | assuming there's more than one. 22 | 23 | :: 24 | import asyncgpio as gpio 25 | 26 | def main(): 27 | with gpio.open_chip(0, consumer="example") as chip: 28 | do_something_with(chip) 29 | 30 | The ``consumer`` argument is optional. It describes your code to the kernel, so that 31 | a program which enumerates GPIO users can display who currently uses the pin in question. 32 | 33 | .. autofunction:: asyncgpio.open_chip 34 | 35 | Referring to a line 36 | ------------------- 37 | 38 | Via the chip object, you can refer to single GPIO lines. 39 | 40 | You need to refer to your hardware's documentation to discover which line to use. 41 | 42 | .. note:: 43 | 44 | Accessing a line in a way not intended by hardware (i.e. driving a line 45 | as an output that's actually an input) may damage your computer or its 46 | periphera(s). 47 | 48 | .. automethod: asyncgpio.gpio.Chip.line 49 | 50 | .. note:: 51 | 52 | ``libgpiod`` has functions for bulk referrals which allow you to access multiple lines 53 | at the same time. Yo may wonder why these are missing from AsyncGPIO. The answer is that 54 | the kernel's GPIO interface does not have functions that affect multiple lines; if you 55 | need them for convenience it's easer to write appropriate Python methods, than to do the 56 | packing+unpacking of values into C-language structures that these bulk methods require. 57 | 58 | 59 | Using a line 60 | ------------ 61 | 62 | A :class:`asyncgpio.gpio.Line` object just describes a GPIO line; before you can actually 63 | use it, you need to request it from the kernel (which also prevents anybody else from using 64 | the line). 65 | 66 | .. note:: Before doing *that*, you might need to de-allocate the line from any 67 | kernel driver that uses it for its own purpose. You typically do that via sysfs. 68 | 69 | This includes the deprecated ``sysfs`` GPIO driver! That is, if you previously marked 70 | a GPIO line for your use by running ``echo 12 >/sys/class/gpio/export`` or similar, 71 | you need to do ``echo 12 >/sys/class/gpio/unexport`` before your program can access 72 | line 12. 73 | 74 | .. automethod: asyncgpio.Line.open 75 | 76 | Output 77 | ~~~~~~ 78 | 79 | Conceptually, controlling on a GPIO line is simple:: 80 | 81 | with chip.line(20).open(direction=gpio.DIRECTION_OUTPUT) as line: 82 | line.value = 1 83 | 84 | However, the code above will not work because as soon as the ``with`` block is exited, 85 | the line is closed. Most likely, this means that the line will revert to its hardware 86 | default. 87 | 88 | Thus, if you want your light to be on for more than a few microseconds, the following 89 | code might be a better idea:: 90 | 91 | with chip.line(20).open(direction=gpio.DIRECTION_OUTPUT) as line: 92 | line.value = 1 93 | try: 94 | await anyio.sleep(5*60) 95 | finally: 96 | line.value = 0 97 | 98 | 99 | Input 100 | ~~~~~ 101 | 102 | Reading a line is easy:: 103 | 104 | with chip.line(20).open(direction=gpio.DIRECTION_INPUT) as line: 105 | if line.value: 106 | print("It's on!") 107 | 108 | Like the previous example, this will immediately close the line, so if you need to access 109 | a value more often (or if you want to make sure that no other program, or a misguided user, 110 | can steal it) it's more efficient to use a long-running ``with`` block. 111 | 112 | Monitoring 113 | ~~~~~~~~~~ 114 | 115 | If you want to watch when an input changes, periodically reading its value is not a good idea. 116 | Polling eats CPU time (thus heating up your computer for no good reason), your 117 | timing will be inaccurate, and your code will be scheduled with low priority because 118 | it is continually busy. 119 | 120 | Therefore, it's better to let the kernel signal changes to a GPIO device:: 121 | 122 | with gpio.Chip(0) as c: 123 | with c.line(19).open(direction=gpio.DIRECTION_OUTPUT) as out_: 124 | wire = c.line(20) 125 | with wire.monitor(gpio.REQUEST_EVENT_BOTH_EDGES): 126 | async for e in in_: 127 | print(e, "on" if e.value else "off", "at", e.time.strftime("%H:%M:%S")) 128 | 129 | :: automethod: asyncgpio.gpio.Line.monitor 130 | 131 | :: autoclass: asyncgpio.gpio.Event 132 | :members: 133 | 134 | .. note:: 135 | 136 | The kernel will send an initial message with the line's current state, thus even when you 137 | request e.g. only notifications for rising edges, the initial event may have a zero value. 138 | 139 | -------------------------------------------------------------------------------- /examples/line_echo.py: -------------------------------------------------------------------------------- 1 | import anyio 2 | import asyncgpio as gpio 3 | """ 4 | This script oggles a pin and watches another. The two are presumed to be connected (hardware wire). 5 | """ 6 | 7 | 8 | async def pling(line): 9 | while True: 10 | await anyio.sleep(1) 11 | line.value = 1 12 | await anyio.sleep(1) 13 | line.value = 0 14 | 15 | 16 | async def main(): 17 | async with anyio.create_task_group() as n: 18 | with gpio.Chip(0) as c: 19 | with c.line(19).open(direction=gpio.DIRECTION_OUTPUT) as out_: 20 | in_ = c.line(20) 21 | await n.spawn(pling, out_) 22 | with in_.monitor(gpio.REQUEST_EVENT_BOTH_EDGES): 23 | async for e in in_: 24 | print(e, "on" if e.value else "off", "at", e.time.strftime("%H:%M:%S")) 25 | 26 | 27 | if __name__ == "__main__": 28 | anyio.run(main, backend="trio") 29 | -------------------------------------------------------------------------------- /examples/line_echo_polled.py: -------------------------------------------------------------------------------- 1 | import anyio 2 | import asyncgpio as gpio 3 | """ 4 | This script oggles a pin and watches another. The two are presumed to be connected (hardware wire). 5 | """ 6 | 7 | 8 | async def pling(line): 9 | while True: 10 | line.value = 1 11 | await anyio.sleep(1) 12 | line.value = 0 13 | await anyio.sleep(1) 14 | 15 | 16 | async def main(): 17 | async with anyio.create_task_group() as n: 18 | with gpio.Chip(0) as c: 19 | with c.line(19).open(direction=gpio.DIRECTION_OUTPUT) as out_, \ 20 | c.line(20).open(direction=gpio.DIRECTION_INPUT) as in_: 21 | await n.spawn(pling, out_) 22 | while True: 23 | print(in_.value) 24 | await anyio.sleep(0.3) 25 | 26 | 27 | if __name__ == "__main__": 28 | anyio.run(main, backend="trio") 29 | -------------------------------------------------------------------------------- /examples/line_value.py: -------------------------------------------------------------------------------- 1 | import asyncgpio as gpio 2 | import time 3 | """Flash an output manually. 4 | 5 | On the Pi3, control the LEDs thus: 6 | # cd /sys/class/leds/led0 ## or /led1 7 | # echo gpio >trigger 8 | # echo 16 >gpio 9 | 10 | Enjoy. 11 | 12 | NB: The red LED can go much faster. Have fun. 13 | The green LED cannot (limited by hardware, 14 | so that you can still see very fast flashes) 15 | 16 | """ 17 | if __name__ == "__main__": 18 | with gpio.Chip(0) as c: 19 | with c.line(16).open(gpio.DIRECTION_OUTPUT) as l: 20 | 21 | try: 22 | while True: 23 | l.value = 1 24 | time.sleep(0.1) 25 | l.value = 0 26 | time.sleep(0.1) 27 | finally: 28 | l.value = 0 29 | -------------------------------------------------------------------------------- /examples/push_button_event.py: -------------------------------------------------------------------------------- 1 | import time 2 | import anyio 3 | import asyncgpio as gpio 4 | 5 | """ 6 | This example is taken out of my furnace controller. 7 | It has been tested with Raspberry Pi Zero W and I assume it will work with any board supported by asyncgpio. 8 | Use at your own risk. 9 | 10 | If you aren't sure about how to hook up a button and led to your board, there are a lot of examples online. 11 | 12 | Thank you @smurfix, who wrote asyncgpio and @njsmith and other in glitter:python-trio/general room 13 | who helped me out. 14 | """ 15 | 16 | 17 | class Led: 18 | # This class turns on and off the power to a pin. 19 | # Two events are setup for turning off an on the pin. Both events need to be 20 | # called at the same time or trio might await at the wrong spot. 21 | def __init__(self, line): 22 | self.x = line 23 | self._on = anyio.create_event() 24 | self._off = anyio.create_event() 25 | 26 | async def liteon(self): 27 | with gpio.open_chip() as chip: 28 | with chip.line(self.x).open(direction=gpio.DIRECTION_OUTPUT) as line: 29 | self._on.clear() 30 | await self._off.set() 31 | while True: 32 | if self._on.is_set(): 33 | line.value = 1 34 | # print('lite on') 35 | await self._off.wait() 36 | self._on = anyio.create_event() 37 | elif self._off.is_set(): 38 | line.value = 0 39 | # print('lite off')d 40 | await self._on.wait() 41 | self._off = anyio.create_event() 42 | else: 43 | # should never be reached. 44 | # if the code does reach here, 45 | # turn off the power to whatever is being powered 46 | print('error: both are off.') 47 | await self._off.set() 48 | 49 | 50 | class Button: 51 | # Add the events tthe button is attached to and the on off event are passed into the class. 52 | # The class listens for the voltage to rise then reverses whatever the current settings are. 53 | def __init__(self, line, event_on, event_off): 54 | self.y = line 55 | self._on = event_on 56 | self._off = event_off 57 | 58 | async def push(self): 59 | with gpio.Chip(0) as c: 60 | in_ = c.line(self.y) 61 | with in_.monitor(gpio.REQUEST_EVENT_RISING_EDGE): 62 | last = 0 63 | async for e in in_: 64 | # This section is for debouncing the button. 65 | # As a button is pushed and released the voltage can rapidly go up and down many times 66 | # when the user only meant one push. To limit this, a delay is add to ignore changes. 67 | # This can be adjusted depending on the button and the respose. 68 | secs, ns_secs = e.timestamp 69 | now = float(str(secs)+'.'+str(ns_secs)) 70 | if now >= last + .25: 71 | print('button', e.value, secs, ns_secs, now) 72 | if self._on.is_set(): 73 | await self._off.set() 74 | else: 75 | await self._on.set() 76 | last = now 77 | 78 | # Asyncgpio uses the BCM pin numbering. So, the led is on the pin 21 79 | # and the button that controls the yellow is hooked to pin 23. 80 | yellow = Led(21) 81 | yellowbutton = Button(23, yellow._on, yellow._off) 82 | 83 | 84 | async def main(y): 85 | async with anyio.create_task_group() as nursery: 86 | await nursery.spawn(yellowbutton.push) 87 | await nursery.spawn(yellow.liteon) 88 | 89 | 90 | if __name__ == "__main__": 91 | anyio.run(main, 1, backend="trio") 92 | -------------------------------------------------------------------------------- /newsfragments/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python-trio/asyncgpio/2761baec88f9970f814ccdbca24c0bc4e363916a/newsfragments/.gitkeep -------------------------------------------------------------------------------- /newsfragments/README.rst: -------------------------------------------------------------------------------- 1 | Adding newsfragments 2 | ==================== 3 | 4 | This directory collects "newsfragments": short files that each contain 5 | a snippet of ReST-formatted text that will be added to the next 6 | release notes. This should be a description of aspects of the change 7 | (if any) that are relevant to users. (This contrasts with your commit 8 | message and PR description, which are a description of the change as 9 | relevant to people working on the code itself.) 10 | 11 | Each file should be named like ``..rst``, where 12 | ```` is an issue numbers, and ```` is one of: 13 | 14 | * ``feature`` 15 | * ``bugfix`` 16 | * ``doc`` 17 | * ``removal`` 18 | * ``misc`` 19 | 20 | So for example: ``123.feature.rst``, ``456.bugfix.rst`` 21 | 22 | If your PR fixes an issue, use that number here. If there is no issue, 23 | then after you submit the PR and get the PR number you can add a 24 | newsfragment using that instead. 25 | 26 | Note that the ``towncrier`` tool will automatically 27 | reflow your text, so don't try to do any fancy formatting. You can 28 | install ``towncrier`` and then run ``towncrier --draft`` if you want 29 | to get a preview of how your change will look in the final release 30 | notes. 31 | 32 | 33 | Making releases 34 | =============== 35 | 36 | ``pip install towncrier``, then run ``towncrier``. (You can use 37 | ``towncrier --draft`` to get a preview of what this will do.) 38 | 39 | You can configure ``towncrier`` (for example: customizing the 40 | different types of changes) by modifying ``pyproject.toml``. 41 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.towncrier] 2 | package = "asyncgpio" 3 | filename = "docs/source/history.rst" 4 | directory = "newsfragments" 5 | underlines = ["-", "~", "^"] 6 | # COOKIECUTTER-TRIO-TODO: fill in the URL below to point to your issue tracker: 7 | issue_format = "`#{issue} `__" 8 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [aliases] 2 | test=pytest 3 | [flake8] 4 | max-line-length=99 5 | ignore=E402,E731,E127,E502,E123,W503 6 | 7 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | LONG_DESC = open("README.rst", encoding="utf-8").read() 4 | 5 | setup( 6 | name="asyncgpio", 7 | use_scm_version={"version_scheme": "guess-next-dev", "local_scheme": "dirty-tag"}, 8 | description="GPIO access via Trio and libgpiod", 9 | url="https://github.com/M-o-a-T/asyncgpio", 10 | long_description=open("README.rst").read(), 11 | author="Matthias Urlichs", 12 | author_email="matthias@urlichs.de", 13 | license="MIT -or- Apache License 2.0", 14 | packages=find_packages(), 15 | setup_requires=["setuptools_scm"], 16 | install_requires=["anyio", "cffi"], 17 | keywords=["gpio"], 18 | python_requires=">=3.6", 19 | classifiers=[ 20 | "License :: OSI Approved :: MIT License", 21 | "License :: OSI Approved :: Apache Software License", 22 | "Framework :: Trio", 23 | "Operating System :: POSIX :: Linux", 24 | "Programming Language :: Python :: 3 :: Only", 25 | "Programming Language :: Python :: Implementation :: CPython", 26 | "Programming Language :: Python :: Implementation :: PyPy", 27 | "Development Status :: 3 - Alpha", 28 | "Intended Audience :: Developers", 29 | "Intended Audience :: Education", 30 | "Topic :: Software Development :: Libraries :: Python Modules", 31 | "Topic :: System :: Hardware :: Hardware Drivers", 32 | ], 33 | ) 34 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python-trio/asyncgpio/2761baec88f9970f814ccdbca24c0bc4e363916a/tests/__init__.py -------------------------------------------------------------------------------- /tests/run.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | # TODO 4 | -------------------------------------------------------------------------------- /tests/test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -uxe 4 | chip="gpio-mockup-A" 5 | cur="$(pwd)" 6 | 7 | rmmod gpio-mockup >/dev/null 2>&1 || true 8 | if lsmod | grep -sq gpio-mockup ; then 9 | echo "Could not remove the gpio-mockup module. Exiting." >&2 10 | exit 1 11 | fi 12 | modprobe gpio-mockup gpio_mockup_ranges=-1,8 13 | cd /sys/class/gpio/ 14 | for d in gpiochip* ; do 15 | if test "$(cat $d/label)" = "$chip" ; then 16 | D=$d 17 | break 18 | fi 19 | done 20 | E="/sys/kernel/debug/gpio-mockup-event/$chip" 21 | H="$(hostname | sed -e 's/\..*//')" 22 | cd "$cur" 23 | 24 | export PYTHONPATH=.:../asyncgpio 25 | 26 | python3 tests/run.py $chip 27 | --------------------------------------------------------------------------------