├── .gitignore ├── LICENSE ├── README.md ├── setup.cfg ├── setup.py └── sysfs ├── __init__.py └── gpio.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | .Python 10 | env/ 11 | bin/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | eggs/ 16 | lib/ 17 | lib64/ 18 | parts/ 19 | sdist/ 20 | var/ 21 | *.egg-info/ 22 | .installed.cfg 23 | *.egg 24 | 25 | # Installer logs 26 | pip-log.txt 27 | pip-delete-this-directory.txt 28 | 29 | # Unit test / coverage reports 30 | htmlcov/ 31 | .tox/ 32 | .coverage 33 | .cache 34 | nosetests.xml 35 | coverage.xml 36 | 37 | # Translations 38 | *.mo 39 | 40 | # Mr Developer 41 | .mr.developer.cfg 42 | .project 43 | .pydevproject 44 | 45 | # Rope 46 | .ropeproject 47 | 48 | # Django stuff: 49 | *.log 50 | *.pot 51 | 52 | # Sphinx documentation 53 | docs/_build/ 54 | 55 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Derek Willian Stavis 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Linux SysFS GPIO access via Python 2 | ================================== 3 | 4 | This package offer Python classes to work with GPIO on Linux. 5 | 6 | ## System Requirements 7 | 8 | As this package relies on modern techniques provided by Linux kernel, so your kernel version should support at least EPoll and SysFS interfaces. 9 | 10 | ## Package Requirements 11 | 12 | This package is based on Twisted main loop. To build the package you will also 13 | need setuptools. 14 | 15 | ## How to use it 16 | 17 | 1. Download this repository 18 | 19 | ```shell 20 | git clone https://github.com/derekstavis/python-sysfs-gpio.git 21 | ``` 22 | 23 | 2. Inside it, issue: 24 | 25 | ```shell 26 | sudo python setup.py install 27 | ``` 28 | 29 | 3. On your code: 30 | 31 | ```python 32 | 33 | # Import Twisted mainloop 34 | 35 | from twisted.internet import reactor 36 | 37 | # Import this package objects 38 | 39 | from sysfs.gpio import Controller, OUTPUT, INPUT, RISING 40 | 41 | # Refer to your chip GPIO numbers and set them this way 42 | 43 | Controller.available_pins = [1, 2, 3, 4] 44 | 45 | # Allocate a pin as Output signal 46 | 47 | pin = Controller.alloc_pin(1, OUTPUT) 48 | pin.set() # Sets pin to high logic level 49 | pin.reset() # Sets pin to low logic level 50 | pin.read() # Reads pin logic level 51 | 52 | # Allocate a pin as simple Input signal 53 | 54 | pin = Controller.alloc_pin(1, INPUT) 55 | pin.read() # Reads pin logic level 56 | 57 | # Allocate a pin as level triggered Input signal 58 | 59 | def pin_changed(number, state): 60 | print("Pin '%d' changed to %d state" % (number, state)) 61 | 62 | pin = Controller.alloc_pin(1, INPUT, pin_changed, RISING) 63 | pin.read() # Reads pin logic level 64 | 65 | ``` 66 | 67 | 4. Don't forget to start reactor loop! 68 | 69 | ```python 70 | reactor.run() 71 | ``` 72 | 73 | 74 | ## Contributing 75 | 76 | If you think that there's work that can be done to make this module better 77 | (and that's the only certainty), fork this repo, make some changes and create 78 | a pull request. I will be glad to accept it! :) 79 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | description-file = README.md 3 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #/usr/bin/env python 2 | 3 | '''The setup and build script for the SysFS GPIO project.''' 4 | 5 | from setuptools import setup, find_packages 6 | 7 | __name__ = 'sysfs-gpio' 8 | __description__ = 'Linux SysFS GPIO Access' 9 | __author__ = 'Derek Willian Stavis' 10 | __version__ = '0.2.2' 11 | __author_email__ = 'dekestavis@gmail.com' 12 | __author_site__ = 'http://derekstavis.github.io' 13 | 14 | requirements = ['Twisted>=13.1.0'] 15 | 16 | setup( 17 | name = __name__, 18 | description = __description__, 19 | version = __version__, 20 | author = __author__, 21 | author_email = __author_email__, 22 | url = __author_site__, 23 | 24 | install_requires = requirements, 25 | include_package_data = True, 26 | 27 | packages = find_packages(), # include all packages under src 28 | ) 29 | -------------------------------------------------------------------------------- /sysfs/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/derekstavis/python-sysfs-gpio/9415a216b7347e7c3754507c31c5cb1fbb33cdc5/sysfs/__init__.py -------------------------------------------------------------------------------- /sysfs/gpio.py: -------------------------------------------------------------------------------- 1 | """ 2 | Linux SysFS-based native GPIO implementation. 3 | 4 | The MIT License (MIT) 5 | 6 | Copyright (c) 2014 Derek Willian Stavis 7 | 8 | Permission is hereby granted, free of charge, to any person obtaining a copy 9 | of this software and associated documentation files (the "Software"), to deal 10 | in the Software without restriction, including without limitation the rights 11 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | copies of the Software, and to permit persons to whom the Software is 13 | furnished to do so, subject to the following conditions: 14 | 15 | The above copyright notice and this permission notice shall be included in all 16 | copies or substantial portions of the Software. 17 | 18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | SOFTWARE. 25 | """ 26 | 27 | __all__ = ('DIRECTIONS', 'INPUT', 'OUTPUT', 28 | 'EDGES', 'RISING', 'FALLING', 'BOTH', 29 | 'Controller') 30 | 31 | import errno 32 | import os 33 | import select 34 | 35 | from twisted.internet import reactor 36 | 37 | import logging 38 | 39 | Logger = logging.getLogger('sysfs.gpio') 40 | Logger.addHandler(logging.StreamHandler()) 41 | Logger.setLevel(logging.DEBUG) 42 | 43 | # Sysfs constants 44 | 45 | SYSFS_BASE_PATH = '/sys/class/gpio' 46 | 47 | SYSFS_EXPORT_PATH = SYSFS_BASE_PATH + '/export' 48 | SYSFS_UNEXPORT_PATH = SYSFS_BASE_PATH + '/unexport' 49 | 50 | SYSFS_GPIO_PATH = SYSFS_BASE_PATH + '/gpio%d' 51 | SYSFS_GPIO_DIRECTION_PATH = SYSFS_GPIO_PATH + '/direction' 52 | SYSFS_GPIO_EDGE_PATH = SYSFS_GPIO_PATH + '/edge' 53 | SYSFS_GPIO_VALUE_PATH = SYSFS_GPIO_PATH + '/value' 54 | SYSFS_GPIO_ACTIVE_LOW_PATH = SYSFS_GPIO_PATH + '/active_low' 55 | 56 | SYSFS_GPIO_VALUE_LOW = '0' 57 | SYSFS_GPIO_VALUE_HIGH = '1' 58 | 59 | EPOLL_TIMEOUT = 1 # second 60 | 61 | # Public interface 62 | 63 | INPUT = 'in' 64 | OUTPUT = 'out' 65 | 66 | RISING = 'rising' 67 | FALLING = 'falling' 68 | BOTH = 'both' 69 | 70 | ACTIVE_LOW_ON = 1 71 | ACTIVE_LOW_OFF = 0 72 | 73 | DIRECTIONS = (INPUT, OUTPUT) 74 | EDGES = (RISING, FALLING, BOTH) 75 | ACTIVE_LOW_MODES = (ACTIVE_LOW_ON, ACTIVE_LOW_OFF) 76 | 77 | 78 | class Pin(object): 79 | """ 80 | Represent a pin in SysFS 81 | """ 82 | 83 | def __init__(self, number, direction, callback=None, edge=None, active_low=0): 84 | """ 85 | @type number: int 86 | @param number: The pin number 87 | @type direction: int 88 | @param direction: Pin direction, enumerated by C{Direction} 89 | @type callback: callable 90 | @param callback: Method be called when pin changes state 91 | @type edge: int 92 | @param edge: The edge transition that triggers callback, 93 | enumerated by C{Edge} 94 | @type active_low: int 95 | @param active_low: Indicator of whether this pin uses inverted 96 | logic for HIGH-LOW transitions. 97 | """ 98 | self._number = number 99 | self._direction = direction 100 | self._callback = callback 101 | self._active_low = active_low 102 | 103 | self._fd = open(self._sysfs_gpio_value_path(), 'r+') 104 | 105 | if callback and not edge: 106 | raise Exception('You must supply a edge to trigger callback on') 107 | 108 | with open(self._sysfs_gpio_direction_path(), 'w') as fsdir: 109 | fsdir.write(direction) 110 | 111 | if edge: 112 | with open(self._sysfs_gpio_edge_path(), 'w') as fsedge: 113 | fsedge.write(edge) 114 | 115 | if active_low: 116 | if active_low not in ACTIVE_LOW_MODES: 117 | raise Exception('You must supply a value for active_low which is either 0 or 1.') 118 | with open(self._sysfs_gpio_active_low_path(), 'w') as fsactive_low: 119 | fsactive_low.write(str(active_low)) 120 | 121 | @property 122 | def callback(self): 123 | """ 124 | Gets this pin callback 125 | """ 126 | return self._callback 127 | 128 | @callback.setter 129 | def callback(self, value): 130 | """ 131 | Sets this pin callback 132 | """ 133 | self._callback = value 134 | 135 | @property 136 | def direction(self): 137 | """ 138 | Pin direction 139 | """ 140 | return self._direction 141 | 142 | @property 143 | def number(self): 144 | """ 145 | Pin number 146 | """ 147 | return self._number 148 | 149 | @property 150 | def active_low(self): 151 | """ 152 | Pin number 153 | """ 154 | return self._active_low 155 | 156 | def set(self): 157 | """ 158 | Set pin to HIGH logic setLevel 159 | """ 160 | self._fd.write(SYSFS_GPIO_VALUE_HIGH) 161 | self._fd.seek(0) 162 | 163 | def reset(self): 164 | """ 165 | Set pin to LOW logic setLevel 166 | """ 167 | self._fd.write(SYSFS_GPIO_VALUE_LOW) 168 | self._fd.seek(0) 169 | 170 | def read(self): 171 | """ 172 | Read pin value 173 | 174 | @rtype: int 175 | @return: I{0} when LOW, I{1} when HIGH 176 | """ 177 | val = self._fd.read() 178 | self._fd.seek(0) 179 | return int(val) 180 | 181 | def fileno(self): 182 | """ 183 | Get the file descriptor associated with this pin. 184 | 185 | @rtype: int 186 | @return: File descriptor 187 | """ 188 | return self._fd.fileno() 189 | 190 | def changed(self, state): 191 | if callable(self._callback): 192 | self._callback(self.number, state) 193 | 194 | def _sysfs_gpio_value_path(self): 195 | """ 196 | Get the file that represent the value of this pin. 197 | 198 | @rtype: str 199 | @return: the path to sysfs value file 200 | """ 201 | return SYSFS_GPIO_VALUE_PATH % self.number 202 | 203 | def _sysfs_gpio_direction_path(self): 204 | """ 205 | Get the file that represent the direction of this pin. 206 | 207 | @rtype: str 208 | @return: the path to sysfs direction file 209 | """ 210 | return SYSFS_GPIO_DIRECTION_PATH % self.number 211 | 212 | def _sysfs_gpio_edge_path(self): 213 | """ 214 | Get the file that represent the edge that will trigger an interrupt. 215 | 216 | @rtype: str 217 | @return: the path to sysfs edge file 218 | """ 219 | return SYSFS_GPIO_EDGE_PATH % self.number 220 | 221 | def _sysfs_gpio_active_low_path(self): 222 | """ 223 | Get the file that represents the active_low setting for this pin. 224 | 225 | @rtype: str 226 | @return: the path to sysfs active_low file 227 | """ 228 | return SYSFS_GPIO_ACTIVE_LOW_PATH % self.number 229 | 230 | 231 | class Controller(object): 232 | ''' 233 | A singleton class to provide access to SysFS GPIO pins 234 | ''' 235 | 236 | def __new__(cls, *args, **kw): 237 | if not hasattr(cls, '_instance'): 238 | instance = super(Controller, cls).__new__(cls) 239 | instance._allocated_pins = {} 240 | instance._poll_queue = select.epoll() 241 | 242 | instance._available_pins = [] 243 | instance._running = True 244 | 245 | # Cleanup before stopping reactor 246 | reactor.addSystemEventTrigger('before', 'shutdown', instance.stop) 247 | 248 | # Run the EPoll in a Thread, as it blocks. 249 | reactor.callInThread(instance._poll_queue_loop) 250 | 251 | cls._instance = instance 252 | return cls._instance 253 | 254 | def __init__(self): 255 | pass 256 | 257 | def _poll_queue_loop(self): 258 | 259 | while self._running: 260 | try: 261 | events = self._poll_queue.poll(EPOLL_TIMEOUT) 262 | except IOError as error: 263 | if error.errno != errno.EINTR: 264 | Logger.error(repr(error)) 265 | reactor.stop() 266 | if len(events) > 0: 267 | reactor.callFromThread(self._poll_queue_event, events) 268 | 269 | @property 270 | def available_pins(self): 271 | return self._available_pins 272 | 273 | @available_pins.setter 274 | def available_pins(self, value): 275 | self._available_pins = value 276 | 277 | def stop(self): 278 | self._running = False 279 | 280 | try: 281 | values = self._allocated_pins.copy().itervalues() 282 | except AttributeError: 283 | values = self._allocated_pins.copy().values() 284 | for pin in values: 285 | self.dealloc_pin(pin.number) 286 | 287 | def alloc_pin(self, number, direction, callback=None, edge=None, active_low=0): 288 | 289 | Logger.debug('SysfsGPIO: alloc_pin(%d, %s, %s, %s, %s)' 290 | % (number, direction, callback, edge, active_low)) 291 | 292 | self._check_pin_validity(number) 293 | 294 | if direction not in DIRECTIONS: 295 | raise Exception("Pin direction %s not in %s" 296 | % (direction, DIRECTIONS)) 297 | 298 | if callback and edge not in EDGES: 299 | raise Exception("Pin edge %s not in %s" % (edge, EDGES)) 300 | 301 | if not self._check_pin_already_exported(number): 302 | with open(SYSFS_EXPORT_PATH, 'w') as export: 303 | export.write('%d' % number) 304 | else: 305 | Logger.debug("SysfsGPIO: Pin %d already exported" % number) 306 | 307 | pin = Pin(number, direction, callback, edge, active_low) 308 | 309 | if direction is INPUT: 310 | self._poll_queue_register_pin(pin) 311 | 312 | self._allocated_pins[number] = pin 313 | return pin 314 | 315 | def _poll_queue_register_pin(self, pin): 316 | ''' Pin responds to fileno(), so it's pollable. ''' 317 | self._poll_queue.register(pin, (select.EPOLLPRI | select.EPOLLET)) 318 | 319 | def _poll_queue_unregister_pin(self, pin): 320 | self._poll_queue.unregister(pin) 321 | 322 | def dealloc_pin(self, number): 323 | 324 | Logger.debug('SysfsGPIO: dealloc_pin(%d)' % number) 325 | 326 | if number not in self._allocated_pins: 327 | raise Exception('Pin %d not allocated' % number) 328 | 329 | with open(SYSFS_UNEXPORT_PATH, 'w') as unexport: 330 | unexport.write('%d' % number) 331 | 332 | pin = self._allocated_pins[number] 333 | 334 | if pin.direction is INPUT: 335 | self._poll_queue_unregister_pin(pin) 336 | 337 | del pin, self._allocated_pins[number] 338 | 339 | def get_pin(self, number): 340 | 341 | Logger.debug('SysfsGPIO: get_pin(%d)' % number) 342 | 343 | return self._allocated_pins[number] 344 | 345 | def set_pin(self, number): 346 | 347 | Logger.debug('SysfsGPIO: set_pin(%d)' % number) 348 | 349 | if number not in self._allocated_pins: 350 | raise Exception('Pin %d not allocated' % number) 351 | 352 | return self._allocated_pins[number].set() 353 | 354 | def reset_pin(self, number): 355 | 356 | Logger.debug('SysfsGPIO: reset_pin(%d)' % number) 357 | 358 | if number not in self._allocated_pins: 359 | raise Exception('Pin %d not allocated' % number) 360 | 361 | return self._allocated_pins[number].reset() 362 | 363 | def get_pin_state(self, number): 364 | 365 | Logger.debug('SysfsGPIO: get_pin_state(%d)' % number) 366 | 367 | if number not in self._allocated_pins: 368 | raise Exception('Pin %d not allocated' % number) 369 | 370 | pin = self._allocated_pins[number] 371 | 372 | if pin.direction == INPUT: 373 | self._poll_queue_unregister_pin(pin) 374 | 375 | val = pin.read() 376 | 377 | if pin.direction == INPUT: 378 | self._poll_queue_register_pin(pin) 379 | 380 | if val <= 0: 381 | return False 382 | else: 383 | return True 384 | 385 | ''' Private Methods ''' 386 | 387 | def _poll_queue_event(self, events): 388 | """ 389 | EPoll event callback 390 | """ 391 | 392 | for fd, event in events: 393 | if not (event & (select.EPOLLPRI | select.EPOLLET)): 394 | continue 395 | 396 | try: 397 | values = self._allocated_pins.itervalues() 398 | except AttributeError: 399 | values = self._allocated_pins.values() 400 | for pin in values: 401 | if pin.fileno() == fd: 402 | pin.changed(pin.read()) 403 | 404 | def _check_pin_already_exported(self, number): 405 | """ 406 | Check if this pin was already exported on sysfs. 407 | 408 | @type number: int 409 | @param number: Pin number 410 | @rtype: bool 411 | @return: C{True} when it's already exported, otherwise C{False} 412 | """ 413 | gpio_path = SYSFS_GPIO_PATH % number 414 | return os.path.isdir(gpio_path) 415 | 416 | def _check_pin_validity(self, number): 417 | """ 418 | Check if pin number exists on this bus 419 | 420 | @type number: int 421 | @param number: Pin number 422 | @rtype: bool 423 | @return: C{True} when valid, otherwise C{False} 424 | """ 425 | 426 | if number not in self._available_pins: 427 | raise Exception("Pin number out of range") 428 | 429 | if number in self._allocated_pins: 430 | raise Exception("Pin already allocated") 431 | 432 | # Create controller instance 433 | Controller = Controller() 434 | 435 | 436 | if __name__ == '__main__': 437 | print("This module isn't intended to be run directly.") 438 | --------------------------------------------------------------------------------