├── .gitignore ├── CHANGELOG.rst ├── CMakeLists.txt ├── README.md ├── include └── gpio_control │ ├── __init__.py │ └── gpio_control_utils.py ├── license.txt ├── msg ├── InputState.msg └── OutputState.msg ├── node └── gpio_control_node ├── package.xml └── setup.py /.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 | -------------------------------------------------------------------------------- /CHANGELOG.rst: -------------------------------------------------------------------------------- 1 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 2 | Changelog for package gpio_control 3 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 4 | 5 | Forthcoming 6 | ----------- 7 | * first public release 8 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /include/gpio_control/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cst0/gpio_control/05671e81ffef4d5a6128875d34dc229206331eba/include/gpio_control/__init__.py -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 -------------------------------------------------------------------------------- /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 -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------