├── include
└── gpio_control
│ ├── __init__.py
│ └── gpio_control_utils.py
├── msg
├── OutputState.msg
└── InputState.msg
├── CHANGELOG.rst
├── setup.py
├── CMakeLists.txt
├── package.xml
├── .gitignore
├── README.md
├── node
└── gpio_control_node
└── license.txt
/include/gpio_control/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/msg/OutputState.msg:
--------------------------------------------------------------------------------
1 | # set the pin to high or low via true/false
2 | bool state
3 |
4 | # Optionally, provide duration for state. <= 0 will leave in state indefinitely.
5 | uint8 duration
--------------------------------------------------------------------------------
/CHANGELOG.rst:
--------------------------------------------------------------------------------
1 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
2 | Changelog for package gpio_control
3 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
4 |
5 | Forthcoming
6 | -----------
7 | * first public release
8 |
--------------------------------------------------------------------------------
/msg/InputState.msg:
--------------------------------------------------------------------------------
1 | # header, which should include things like time stamp data.
2 | Header header
3 |
4 | # the state of the pin. true means high, false means low.
5 | bool state
6 |
7 | # the pin that has changed. Represented as a string because
8 | # some pins are named things like '12', while others are named
9 | # things like 'P8' depending on your hardware.
10 | string pin
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | """
2 | Setup.py. Makes sure that we have access to our gpio_control_utils lib, both within our node
3 | and within other workspaces if that's something people want to use. Also make sure we can run the
4 | no-root script, if necessary.
5 | """
6 |
7 | from distutils.core import setup
8 | from catkin_pkg.python_setup import generate_distutils_setup
9 |
10 | d = generate_distutils_setup(
11 | packages=['gpio_control'],
12 | package_dir={'': 'include'},
13 | )
14 |
15 | setup(**d)
16 |
--------------------------------------------------------------------------------
/CMakeLists.txt:
--------------------------------------------------------------------------------
1 | cmake_minimum_required(VERSION 2.8.3)
2 | project(gpio_control)
3 |
4 | find_package(catkin REQUIRED COMPONENTS
5 | roscpp
6 | rospy
7 | std_msgs
8 | message_generation
9 | )
10 |
11 | catkin_python_setup()
12 |
13 | # Generate messages in the 'msg' folder
14 | add_message_files(
15 | FILES
16 | InputState.msg
17 | OutputState.msg
18 | )
19 |
20 | generate_messages(
21 | DEPENDENCIES
22 | std_msgs
23 | )
24 |
25 | catkin_package(
26 | CATKIN_DEPENDS message_runtime
27 | )
28 |
29 | include_directories(
30 | ${catkin_INCLUDE_DIRS}
31 | )
32 |
--------------------------------------------------------------------------------
/package.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | gpio_control
4 | 1.0.0
5 | Control GPIO pins on Raspberry Pi, Nvidia Jetson, and other Linux devices with GPIO pins
6 |
7 | cst
8 | Apache 2.0
9 | https://github.com/cst0/gpio_control
10 |
11 | message_generation
12 | rospy
13 | std_msgs
14 | rospy
15 | std_msgs
16 | catkin
17 | message_runtime
18 | rospy
19 | std_msgs
20 |
21 |
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.swp
2 | *swo
3 |
4 |
5 | # Byte-compiled / optimized / DLL files
6 | __pycache__/
7 | *.py[cod]
8 | *$py.class
9 |
10 | # C extensions
11 | *.so
12 |
13 | # Distribution / packaging
14 | .Python
15 | build/
16 | develop-eggs/
17 | dist/
18 | downloads/
19 | eggs/
20 | .eggs/
21 | lib/
22 | lib64/
23 | parts/
24 | sdist/
25 | var/
26 | wheels/
27 | share/python-wheels/
28 | *.egg-info/
29 | .installed.cfg
30 | *.egg
31 | MANIFEST
32 |
33 | # PyInstaller
34 | # Usually these files are written by a python script from a template
35 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
36 | *.manifest
37 | *.spec
38 |
39 | # Installer logs
40 | pip-log.txt
41 | pip-delete-this-directory.txt
42 |
43 | # Unit test / coverage reports
44 | htmlcov/
45 | .tox/
46 | .nox/
47 | .coverage
48 | .coverage.*
49 | .cache
50 | nosetests.xml
51 | coverage.xml
52 | *.cover
53 | *.py,cover
54 | .hypothesis/
55 | .pytest_cache/
56 | cover/
57 |
58 | # Translations
59 | *.mo
60 | *.pot
61 |
62 | # Django stuff:
63 | *.log
64 | local_settings.py
65 | db.sqlite3
66 | db.sqlite3-journal
67 |
68 | # Flask stuff:
69 | instance/
70 | .webassets-cache
71 |
72 | # Scrapy stuff:
73 | .scrapy
74 |
75 | # Sphinx documentation
76 | docs/_build/
77 |
78 | # PyBuilder
79 | .pybuilder/
80 | target/
81 |
82 | # Jupyter Notebook
83 | .ipynb_checkpoints
84 |
85 | # IPython
86 | profile_default/
87 | ipython_config.py
88 |
89 | # pyenv
90 | # For a library or package, you might want to ignore these files since the code is
91 | # intended to run in multiple environments; otherwise, check them in:
92 | # .python-version
93 |
94 | # pipenv
95 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
96 | # However, in case of collaboration, if having platform-specific dependencies or dependencies
97 | # having no cross-platform support, pipenv may install dependencies that don't work, or not
98 | # install all needed dependencies.
99 | #Pipfile.lock
100 |
101 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow
102 | __pypackages__/
103 |
104 | # Celery stuff
105 | celerybeat-schedule
106 | celerybeat.pid
107 |
108 | # SageMath parsed files
109 | *.sage.py
110 |
111 | # Environments
112 | .env
113 | .venv
114 | env/
115 | venv/
116 | ENV/
117 | env.bak/
118 | venv.bak/
119 |
120 | # Spyder project settings
121 | .spyderproject
122 | .spyproject
123 |
124 | # Rope project settings
125 | .ropeproject
126 |
127 | # mkdocs documentation
128 | /site
129 |
130 | # mypy
131 | .mypy_cache/
132 | .dmypy.json
133 | dmypy.json
134 |
135 | # Pyre type checker
136 | .pyre/
137 |
138 | # pytype static type analyzer
139 | .pytype/
140 |
141 | # Cython debug symbols
142 | cython_debug/
143 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # gpio\_control: a ROS package for reading/writing GPIO states on the Pi, Jetson, and many more
2 | ## Overview
3 | Devices such as the Raspberry Pi, NVidia Jetson, BeagleBone Black, etc have GPIO pins as an additional set of IO,
4 | which can be toggled between high/low states or configured to read high/low inputs. This package allows for
5 | ROS control of these pins, allowing for configuring each pin as desired and then reading/writing
6 | them as a ROS node. Additionally, an API which is consistent from device to device is provided,
7 | which aims to make porting robots from platfrom to platform much easier.
8 |
9 | This package has been most thoroughly tested on ROS Melodic running on the NVidia Jetson Nano B01
10 | and on ROS Kinetic running on the Raspberry Pi 3 B+, but has been designed with other platforms and
11 | ROS versions in mind. Please file an issue if this package does not behave as expected. Bug fixes
12 | and contributions, especially ones which support new devices, are welcomed.
13 |
14 | The `gpio_control_node` node allows for control of the GPIO of generic Linux systems. This is done
15 | through the standard Linux /dev/sys/gpio filesystem, and therefore allows for support of all
16 | Linux devices with GPIO pins which conform to this standard. This can be specified using the
17 | `--device generic` flag.
18 |
19 | Additionally, devices such as the Raspberry Pi, which have their own GPIO software API, are
20 | supported via this API in the same node. `gpio_control_utils.py` provides the backbone for this
21 | node, and can also be imported to provide standardized GPIO control across the variety of
22 | machines this package supports. See the section on 'gpio_control_utils' for more information.
23 |
24 | NOTE: Writing GPIO control states via the filesystem may include writing to pins which are in use
25 | by the operating system, which can have unexpected consequences (loss of SD card data, crashes, etc).
26 | It is recommended that you use the device-specific flag, which will perform safety checks and prevent
27 | you from doing anything too damaging. There are device specific flags for the Raspberry Pi's (`--device pi`),
28 | Nvidia Jetson's (`--device jetson`), BeagleBone Black (`--device beaglebone-experimental`),
29 | and Onion Omega (`--device onion-experimental`).
30 |
31 | The final device flag is in the form of `--device simulated`. This flag prevents any actual hardware
32 | manipulation, which makes it useful for running nodes in simulation.
33 |
34 | Python dependencies should already be installed if you are using a device-specific API on its
35 | official operating system. If they are not installed properly, the node will notice and provide
36 | some next steps for you to take.
37 |
38 | ## Usage
39 | Each node accepts the same command line parameters. In the following command, `gpio_control` is used
40 | to create an input on pin 12 of the Raspberry Pi:
41 |
42 | ```
43 | rosrun gpio_control gpio_control --device pi --input 12
44 | ```
45 |
46 | Upon running this command, there should now be a topic titled `/gpio_outputs/twelve` which
47 | is publishing a `gpio_control/InputState` message.
48 |
49 | Creating an output is similar:
50 | ```
51 | rosrun gpio_control gpio_control --device pi --output 12
52 | ```
53 |
54 | Upon running this command, there should now be a topic titled `/gpio_outputs/twelve`
55 | which will publish a `gpio_control/OutputState` upon a state change. The rate at which
56 | the node will check for a state change can be specified using `--rate`. Alternatively,
57 | the current state of a pin can be published continuously at the rate using `--constant-publish`.
58 |
59 | It is possible to control multiple multiple pins with one node:
60 | ```
61 | rosrun gpio_control gpio_control_node --device generic --output 12 13 14 --input 15 16 17
62 | ```
63 |
64 | It is also possible to make a pin both an input and an output, depending on the device-specific
65 | implementation. This may be useful for error checking.
66 |
67 | ## gpio_control_utils.py
68 | The `gpio_control_node` operates by providing a command line interface to the `gpio_control_utils`
69 | python package, which will be importable in other projects upon installation. This provides a
70 | standard and stable interface for reading and writing to GPIO pins. See the node for example
71 | usage.
72 |
--------------------------------------------------------------------------------
/node/gpio_control_node:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 |
3 | """
4 | Control GPIO pins via ROS. Made to be as generic as possible, allowing the same node to be
5 | used in multiple configurations/devices.
6 |
7 | @author cst
8 | @version 0.0.1
9 | @license Apache 2.0
10 | @copyright Christopher Thierauf 2020.
11 | This copyright is used to release the code in accordance with the license of this repository.
12 | """
13 | import sys
14 | import rospy
15 | import argparse
16 |
17 | # If your IDE is complaining about this line, make sure you've set
18 | # /install/lib/dist-packages as a source
19 | from gpio_control.gpio_control_utils import GpioControl, VALID_DEVICES
20 |
21 |
22 | def setup_parser():
23 | parser = argparse.ArgumentParser()
24 | parser.add_argument('-i', '--input', nargs='+',
25 | help='List pins on which to take inputs, allowing this node to publish '
26 | 'the current state of the pin. Either --input or --output must be '
27 | 'used.')
28 | parser.add_argument('-o', '--output', nargs='+',
29 | help="List pins on which to produce outputs, allowing this node to set up a"
30 | "subscriber to take inputs for controlling the state of the pin. "
31 | "Either --input or --output must be used.")
32 |
33 | parser.add_argument('--device', type=str,
34 | help='hardware device to use. Valid devices: ' + str(VALID_DEVICES) +
35 | '. file-system should support all Linux devices but should be used '
36 | 'with caution as there are no safety checks, simulated will only '
37 | 'print to the screen and is not useful in production.')
38 |
39 | parser.add_argument('--rate', type=int, help='Rate at which to run this node. Default: 10.')
40 | parser.add_argument('--constant-publish', action='store_true', help='Rather than only '
41 | 'publishing the state of '
42 | 'inputs on state change, '
43 | 'be constantly publishing '
44 | 'the state of all pins.')
45 |
46 | return parser
47 |
48 |
49 | def check_valid_args(local_args):
50 | if local_args.device is None:
51 | rospy.logwarn("No device was specified, so we're assuming nothing and closing.")
52 | sys.exit(1)
53 | if not local_args.input and not local_args.output:
54 | rospy.logerr("No inputs or outputs specified, so nothing to do here and closing. Provide "
55 | "pins to manipulate using --input or --output. See --help for more "
56 | "information.")
57 | sys.exit(1)
58 |
59 |
60 | if __name__ == '__main__':
61 |
62 | args = setup_parser().parse_args(rospy.myargv()[1:])
63 | check_valid_args(args)
64 |
65 | rospy.init_node("gpio_pin_controller", anonymous=False)
66 |
67 | # Set up a string to inform the user as to what this node will try to do
68 | intro_string = "Hello! Setting up to control GPIO pins "
69 | if args.input is not None:
70 | intro_string += str(args.input) + " as inputs"
71 | if args.output is not None:
72 | # only bother saying 'and' if there's any pins to do that for
73 | intro_string += ", and pins "
74 | if args.output is not None:
75 | intro_string += str(args.output) + " as outputs"
76 | intro_string += "."
77 | rospy.loginfo(intro_string)
78 |
79 | gpio = GpioControl(args.device)
80 | if args.input is not None:
81 | for input_pin in args.input:
82 | gpio.add_input_pin(input_pin)
83 | else:
84 | args.input = [] # be an empty list, not None
85 |
86 | if args.output is not None:
87 | for output_pin in args.output:
88 | gpio.add_output_pin(output_pin)
89 | else:
90 | args.output = [] # be an empty list, not None
91 |
92 | rospy.loginfo("All set up! Ready to accept commands for outputs and provide updates on inputs.")
93 | gpio.spin(args.rate)
94 |
95 | rospy.loginfo("Manager for GPIO pins " +
96 | str(args.input + args.output) +
97 | " stopping. Goodbye!")
98 |
--------------------------------------------------------------------------------
/license.txt:
--------------------------------------------------------------------------------
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 |
204 |
--------------------------------------------------------------------------------
/include/gpio_control/gpio_control_utils.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 |
3 | """
4 | Control GPIO pins via ROS. Made to be as generic as possible, allowing the same node to be
5 | used in multiple configurations/devices.
6 |
7 | @author cst
8 | @version 0.0.1
9 | @license Apache 2.0
10 | @copyright Christopher Thierauf 2020.
11 |
12 | The copyright holder uses this copyright to release the code in accordance with the license of
13 | this repository, which is a Free and Open Source license: you may use, modify, and share this
14 | code in any way you see fit as described by the terms of the license. You are not legally
15 | obligated to provide attribution, but it would be greatly appreciated.
16 | """
17 |
18 | # core stuff
19 | import os
20 | import random
21 | import subprocess
22 | import sys
23 |
24 | import rospy
25 |
26 | # messages
27 | from std_msgs.msg import Header
28 | from gpio_control.msg import InputState, OutputState
29 |
30 | # a list of devices we have support for
31 | VALID_DEVICES = [
32 | # pi support via pigpio
33 | 'pi',
34 | # jetson support also via pigpio
35 | 'jetson',
36 | # beaglebone support via Adafruit_BBIO
37 | 'beaglebone-experimental',
38 | # onion support via onionGpio, though I don't know how popular an option this is.
39 | 'onion-experimental',
40 | # Support of standard Linux systems via the LFS
41 | 'generic',
42 | # Don't actually do anything, just print to the screen
43 | 'simulated'
44 | ]
45 |
46 | # rate at which to run the node (by default)
47 | _DEFAULT_RATE_VAL = 10
48 |
49 | # valid imports are going to depend on our hardware and what's installed. We'll try to import
50 | # everything we might use, and if we fail, keep track of it for later.
51 | _IMPORTED_PIGPIO = False
52 | _IMPORTED_ADAFRUIT_BBIO = False
53 | _IMPORTED_ONION_GPIO = False
54 | _IMPORTED_GPIO_API = False
55 |
56 | try:
57 | import pigpio
58 |
59 | _IMPORTED_PIGPIO = True
60 | _IMPORTED_GPIO_API = True
61 | except ImportError:
62 | pass
63 |
64 | try:
65 | import Adafruit_BBIO.GPIO as BBGPIO
66 |
67 | _IMPORTED_ADAFRUIT_BBIO = True
68 | _IMPORTED_GPIO_API = True
69 | except ImportError:
70 | pass
71 |
72 | try:
73 | import onionGpio
74 |
75 | _IMPORTED_ONION_GPIO = True
76 | _IMPORTED_GPIO_API = True
77 | except ImportError:
78 | pass
79 |
80 | # This was originally done using the 'word2num' python package, but it's pretty simple
81 | # to implement via this stack overflow post:
82 | # https://stackoverflow.com/questions/19504350/how-to-convert-numbers-to-words-without-using-num2word-library#19504396
83 | # So we're removing a dependency here.
84 | _num2words1 = {1: 'One', 2: 'Two', 3: 'Three', 4: 'Four', 5: 'Five',
85 | 6: 'Six', 7: 'Seven', 8: 'Eight', 9: 'Nine', 10: 'Ten',
86 | 11: 'Eleven', 12: 'Twelve', 13: 'Thirteen', 14: 'Fourteen',
87 | 15: 'Fifteen', 16: 'Sixteen', 17: 'Seventeen', 18: 'Eighteen', 19: 'Nineteen'}
88 | _num2words2 = ['Twenty', 'Thirty', 'Forty', 'Fifty', 'Sixty', 'Seventy', 'Eighty', 'Ninety']
89 |
90 |
91 | def _num2word(num: int):
92 | if 0 <= num <= 19:
93 | return _num2words1[num]
94 | elif 20 <= num <= 99:
95 | tens, remainder = divmod(num, 10)
96 | return _num2words2[tens-2] + _num2words1[remainder] if remainder else _num2words2[tens-2]
97 | else:
98 | return _num2word(int(num/10)) + "_thousand"
99 |
100 |
101 | class _GenericOutputPin:
102 | """
103 | Class to provide consistent function calls to different pin outputs.
104 |
105 | Takes in three functions as arguments, which allows us to call 'configure', 'set_low',
106 | and 'set_high' on this class later, despite it being any actual implementation of gpio control
107 | Also provide the option of an 'additional_shutdown' to run on close() if necessary.
108 |
109 | We're using functional programming here and elsewhere. If you aren't familiar with that, give
110 | that topic a quick read: https://docs.python.org/3/howto/functional.html.
111 | If you're reading this because you'd like to contribute (thanks!) but are feeling daunted,
112 | please feel free to open an issue on the tracker so I can work with you on that.
113 | """
114 |
115 | def __init__(self, configured_pinbus_object, pin, set_high_=None, set_low_=None):
116 | self.type = 'output'
117 | if configured_pinbus_object is not None:
118 | self.configured_pinbus_object = configured_pinbus_object
119 | self.set_high_func = set_high_
120 | self.set_low_func = set_low_
121 | self.pin = pin
122 |
123 | def set_low(self):
124 | """ Pull the pin low (0v), but don't try to run None """
125 | return self.set_low_func() if self.set_low_func is not None else None
126 |
127 | def set_high(self):
128 | """ Pull the pin high (typically 3.3v or 5v), but don't try to run None """
129 | return self.set_high_func() if self.set_high_func is not None else None
130 |
131 | def close(self):
132 | """ Make sure we're cleaning up while we shut down here """
133 | self.set_low()
134 |
135 |
136 | class _GenericInputPin:
137 | """
138 | Class to provide consistent function calls to different pin inputs.
139 |
140 | As before, takes in functions to be called later.
141 | """
142 |
143 | def __init__(self, configured_pinbus_object, pin, get_=None):
144 | self.type = 'input'
145 | if configured_pinbus_object is not None:
146 | self.configured_pinbus_object = configured_pinbus_object
147 | self.get_func = get_
148 | self.pin = pin
149 |
150 | def get(self):
151 | """ Get the current state of the GPIO pin. Returns true/false to indicate high/low """
152 | return self.get_func()
153 |
154 |
155 | def _configure_input(pin, device: str, bus):
156 | """
157 | Configure the node as an input. Takes in a pin to access, and a string which is what was
158 | passed in at the command line.
159 | """
160 |
161 | # Run through our options, configure the generic input interfaces, and then return.
162 | if device in ('pi', 'jetson'):
163 | return_input = _GenericInputPin(bus, int(pin))
164 | return_input.get_func = lambda: return_input.configured_pinbus_object.read(int(pin))
165 | return return_input
166 |
167 | if device == 'beaglebone':
168 | BBGPIO.setup(pin, BBGPIO.IN)
169 | return _GenericInputPin(bus, pin, (lambda: BBGPIO.input(pin)))
170 |
171 | if device == 'onion':
172 | return_input = _GenericInputPin(bus, pin)
173 | return_input.configured_pinbus_object.setInputDirection()
174 | return_input.get_func = return_input.configured_pinbus_object.getValue
175 | return return_input
176 |
177 | if device == 'generic':
178 | # todo: o boy do i hate this implementation
179 | os.system('echo ' + str(pin) + ' > /sys/class/gpio/export')
180 | os.system('echo in > /sys/class/gpio/gpio' + str(pin) + '/direction')
181 |
182 | return _GenericInputPin(bus,
183 | pin,
184 | lambda: int(subprocess.check_output(
185 | ['cat', '/sys/class/gpio/gpio' + str(pin) + '/value']
186 | ).decode("utf-8"))
187 | )
188 |
189 | if device == 'simulated':
190 | return _GenericInputPin(None, pin, get_=(lambda: random.choice([True, False])))
191 |
192 | raise RuntimeError('Device was invalid: ' + device)
193 |
194 |
195 | def _configure_output(pin: int, device: str, bus):
196 | """
197 | Configure the node as an output. Takes in a pin to access, and a string which is what was
198 | passed in at the command line.
199 | """
200 | if device in ('pi', 'jetson'):
201 | # as with configure_input, we can use pigpio for both
202 | return_output = _GenericOutputPin(bus, pin)
203 | return_output.set_low_func = (
204 | lambda: return_output.configured_pinbus_object.write(int(pin), pigpio.LOW)
205 | )
206 | return_output.set_high_func = (
207 | lambda: return_output.configured_pinbus_object.write(int(pin), pigpio.HIGH)
208 | )
209 | return return_output
210 |
211 | if device == 'beaglebone':
212 | return _GenericOutputPin(
213 | lambda: BBGPIO.setup(pin, BBGPIO.OUT),
214 | lambda: BBGPIO.output(pin, BBGPIO.LOW),
215 | lambda: BBGPIO.output(pin, BBGPIO.HIGH),
216 | )
217 |
218 | if device == 'onion':
219 | return_output = _GenericOutputPin(onionGpio.OnionGpio(pin), pin)
220 | return_output.configured_pinbus_object.setOutputDirection()
221 | return_output.set_low_func = lambda: return_output.configured_pinbus_object.setValue(0)
222 | return_output.set_high_func = lambda: return_output.configured_pinbus_object.setValue(1)
223 | return return_output
224 |
225 | if device == 'generic':
226 | # todo: as above
227 | os.system('echo ' + str(pin) + ' > /sys/class/gpio/export')
228 | os.system('echo out > /sys/class/gpio/gpio' + str(pin) + '/direction')
229 |
230 | return _GenericOutputPin(None,
231 | pin,
232 | set_low_=lambda: os.system(
233 | 'echo 0 > /sys/class/gpio/gpio' + str(pin) + '/value'),
234 | set_high_=lambda: os.system(
235 | 'echo 1 > /sys/class/gpio/gpio' + str(pin) + '/value')
236 | )
237 |
238 | if device == 'simulated':
239 | return _GenericOutputPin(None, pin, set_high_=(lambda: print("[simulated] high!")),
240 | set_low_=(lambda: print("[simulated] low!")))
241 |
242 | raise RuntimeError('Device was invalid: ' + device)
243 |
244 |
245 | def _to_valid_ros_topic_name(input_string):
246 | """
247 | Convert input to a valid ROS name (alphabetic). This is necessary because ROS best practice is
248 | to have topic names be only alphabetic (hyphens/slashes optional). Most GPIO pins will have
249 | numbers in them, which can be an issue.
250 | """
251 |
252 | # Don't bother dealing with things that are just numbers, convert them right away
253 | if isinstance(input_string, int) or input_string.isnumeric():
254 | return _num2word(int(input_string)).lower()
255 |
256 | # If it's alphabetic, convert numbers to characters, append numbers, and separate the
257 | # two using underscores. Creates something like P8U4 -> p_eight_u_4
258 | output_string = ""
259 | just_did_str = False
260 | just_did_num = False
261 | for character in input_string:
262 | if character.isalpha():
263 | if just_did_num:
264 | output_string += '_'
265 | just_did_num = False
266 | output_string += character.lower()
267 | just_did_str = True
268 | elif character.isnumeric():
269 | if just_did_str:
270 | output_string += '_'
271 | just_did_str = False
272 | output_string += _num2word(character).lower()
273 | just_did_num = True
274 | else:
275 | # Don't bother putting in special characters (even though I don't think they'll come up)
276 | pass
277 |
278 | return output_string
279 |
280 |
281 | def configure_bus(device):
282 | """
283 | Configure a GPIO bus for the specific hardware we're dealing with. Return an object of the
284 | appropriate hardware type, if the specific implementation requires that.
285 | """
286 | # Both the Pi and Jetson have the same pinout and can use the same lib.
287 | if device in ('pi', 'jetson'):
288 | if not _IMPORTED_PIGPIO:
289 | rospy.logfatal("You want to control your device with pigpio, but it didn't import "
290 | "properly. Node will exit.")
291 | sys.exit(2)
292 |
293 | return pigpio.pi()
294 |
295 | if device == 'beaglebone':
296 | rospy.loginfo("This implementation is currently in beta. If you encounter bugs, please "
297 | "report them!")
298 | if not _IMPORTED_ADAFRUIT_BBIO:
299 | rospy.logfatal("You want to control your device using the Adafruit BeagleBone Black "
300 | "GPIO library, but it didn't import properly. Is it installed? Node "
301 | "will exit.")
302 | sys.exit(2)
303 |
304 | if device == 'onion':
305 | rospy.loginfo("This implementation is currently in beta. If you encounter bugs, please "
306 | "report them!")
307 | if not _IMPORTED_ONION_GPIO:
308 | rospy.logfatal("You want to control your device using the Onion Omega "
309 | "GPIO library, but it didn't import properly. Is it installed? Node "
310 | "will exit.")
311 | sys.exit(2)
312 |
313 | if device == 'generic':
314 | rospy.logwarn('You are using the generic GPIO controller, which operates using the Linux '
315 | 'FHS to control GPIO states. It should not be considered as stable or safe '
316 | 'as non-generic options.')
317 |
318 | return None
319 |
320 |
321 | def configure_cleanup(device):
322 | """
323 | If the device in question requires cleanup functions, give them a run.
324 | """
325 | if device == 'beaglebone':
326 | return BBGPIO.cleanup()
327 |
328 | if device == 'generic':
329 | unexporter = open('/sys/class/gpio/unexport', 'w')
330 | # unexporter.write(str(pin)) # todo: make this a 'foreach' in gpio dir. Nothing bad will happen if we don't (probably), it's just to be nice and clean up after ourselves.
331 | unexporter.close()
332 |
333 | return None
334 |
335 |
336 | class GpioControl:
337 | """
338 | Generic control of a GPIO device. Wraps the setup and then provides a spinner to run.
339 | """
340 |
341 | def __init__(self, device: str):
342 | self._device = device
343 | self._publishers = {}
344 | self._generic_pin_objects = {}
345 | self._subscribers = {}
346 | self._bus = configure_bus(device)
347 | self._cleanup = configure_cleanup(device)
348 |
349 | if device not in VALID_DEVICES:
350 | rospy.logerr("I don't know that device (" + device + "). Valid devices: " +
351 | str(VALID_DEVICES) + "\nExiting.")
352 | sys.exit(1)
353 |
354 | if device not in ['generic', 'simulated'] and not _IMPORTED_GPIO_API:
355 | rospy.logfatal("You're trying to use a device-specific API, but we weren't able to "
356 | "import the appropriate Python library. Please make sure that you have "
357 | "the right library installed properly:\n "
358 | "\t* Raspberry Pi: pigpio \n"
359 | "\t* Nvidia Jetson: pigpio\n"
360 | "\t* Beaglebone Black: Adafruit_BBIO\n"
361 | "\t* Onion Omega: onionGpio\n"
362 | "Cannot recover, exiting.")
363 | sys.exit(3)
364 |
365 | def add_input_pin(self, pin):
366 | """ Add a pin to perform input IO operations. """
367 | input_pin_obj = _configure_input(pin, self._device, self._bus)
368 | self._generic_pin_objects[pin] = input_pin_obj
369 | self._publishers[pin] = rospy.Publisher("gpio_inputs/" + _to_valid_ros_topic_name(pin),
370 | InputState,
371 | queue_size=1)
372 |
373 | def add_output_pin(self, pin):
374 | """ Add a pin to perform output IO operations. """
375 | output_pin_obj = _configure_output(pin, self._device, self._bus)
376 |
377 | def subscriber_callback(msg):
378 | """
379 | Subscriber callback.
380 | Called every time a message comes in telling us to change a pin state.
381 | """
382 | if msg.state:
383 | output_pin_obj.set_high_func()
384 | elif not msg.state:
385 | output_pin_obj.set_low_func()
386 | else:
387 | rospy.logerr("Not sure how to deal with " + str(msg))
388 |
389 | self._generic_pin_objects[pin] = output_pin_obj
390 | self._subscribers[pin] = rospy.Subscriber("gpio_outputs/" + _to_valid_ros_topic_name(pin),
391 | OutputState,
392 | subscriber_callback)
393 |
394 | def spin(self, rate_val: int = None):
395 | """ Wrapping the spinner function. """
396 | # Here's where we're doing the actual spinning: read the pin, set up a message, publish,
397 | # rate.sleep(), repeat.
398 | if rate_val is None:
399 | rate_val = _DEFAULT_RATE_VAL
400 | rate = rospy.Rate(rate_val)
401 | while not rospy.is_shutdown():
402 | for pin_obj in self._generic_pin_objects.values():
403 | if pin_obj.type == 'input':
404 | val = pin_obj.get()
405 |
406 | header = Header()
407 | header.stamp = rospy.Time.now()
408 | # Different implementations might give us this in int or bool form.
409 | # Check both and do the appropriate thing with each.
410 | if (isinstance(val, int) and val == 1) or (isinstance(val, bool) and val):
411 | val = True
412 | elif (isinstance(val, int) and val == 0) or (isinstance(val, bool) and not val):
413 | val = False
414 | else:
415 | rospy.logerr("Not sure how to deal with " + str(val))
416 |
417 | try:
418 | self._publishers[str(pin_obj.pin)].publish(
419 | InputState(header, val, str(pin_obj.pin))
420 | )
421 | except KeyError:
422 | rospy.logfatal_once("KeyError, you tried getting " +
423 | str(pin_obj.pin) + " but only " +
424 | str(self._publishers.keys()) +
425 | " is acceptable")
426 |
427 | rate.sleep()
428 |
429 | def set_pin(self, pin, state: bool):
430 | """
431 | If using this code as an import, provide a simple function to set the pin.
432 | """
433 | if pin not in self._generic_pin_objects.keys():
434 | rospy.logerr("The pin you requested (" + str(pin) +
435 | ") isn't in the list of ones we know about: " +
436 | str(self._generic_pin_objects.keys()))
437 |
438 | if not self._generic_pin_objects[pin].type == 'output':
439 | raise EnvironmentError("This pin is not an output! Can't set the state.")
440 |
441 | if state:
442 | pin.set_high()
443 | else:
444 | pin.set_low()
445 |
446 | def get_pin(self, pin):
447 | """
448 | If using this code as an import, provide a simple function to get the state of the pin.
449 | """
450 | if pin not in self._generic_pin_objects.keys():
451 | rospy.logerr("The pin you requested (" + str(pin) +
452 | ") isn't in the list of ones we know about: " +
453 | str(self._generic_pin_objects.keys()))
454 |
455 | if not self._generic_pin_objects[pin].type == 'output':
456 | raise EnvironmentError("This pin is not an output! Can't set the state.")
457 |
458 | return pin.get()
459 |
--------------------------------------------------------------------------------