/** * Copyright (c) 2010-2016 by the respective copyright holders. * * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html */ package org.openhab.io.gpio.linux; import java.io.IOException; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.HashSet; import java.util.Set; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.ReentrantReadWriteLock; import org.openhab.io.gpio.GPIOPin; import org.openhab.io.gpio.GPIOPinEventHandler; import com.sun.jna.LastErrorException; import com.sun.jna.NativeLong; import com.sun.jna.ptr.ByteByReference; /** * Implementation of <code>GPIOPin</code> interface for boards running * Linux OS. Based on kernel GPIO framework exposed to user space * through <code>sysfs</code> pseudo filesystem. * * @author Dancho Penev * @since 1.5.0 */ public class GPIOPinLinux implements GPIOPin { private static final Charset DEFAULT_ENCODING = StandardCharsets.UTF_8; private static final long PINLOCK_TIMEOUT = 10; private static final TimeUnit PINLOCK_TIMEOUT_UNITS = TimeUnit.SECONDS; /** Pin wide read/write lock. */ private ReentrantReadWriteLock pinLock = new ReentrantReadWriteLock(); /** Set of registered pin event handlers. */ private Set<GPIOPinEventHandler> eventHandlers = new HashSet<GPIOPinEventHandler>(); /** * Interrupt listening thread, running only if there are registered * event handlers. */ private EventListener eventListenerThread = null; private int pinNumber; private long debounceInterval; private Path activelowPath = null; private Path directionPath = null; private Path edgePath = null; private Path valuePath = null; /** * Initializes paths to special files exposed to user space by kernel * GPIO framework. * * @param pinNumber the pin number as seen by the kernel * @param gpioPinDirectory path to pin directory in <code>sysfs</code>, * e.g. "/sys/class/gpio/gpio1" * @param debounceInterval default debounce interval */ public GPIOPinLinux(int pinNumber, String gpioPinDirectory, long debounceInterval) { this.pinNumber = pinNumber; activelowPath = Paths.get(gpioPinDirectory + "/active_low"); /* * The 'direction' attribute will not exist if the kernel doesn't * support changing the direction of a GPIO, or it was exported by * kernel code that didn't explicitly allow user space to reconfigure * this GPIO's direction. */ if (Files.exists(Paths.get(gpioPinDirectory + "/direction"))) { directionPath = Paths.get(gpioPinDirectory + "/direction"); } /* * This file exists only if the pin can be configured as an interrupt * generating input pin. */ if (Files.exists(Paths.get(gpioPinDirectory + "/edge"))) { edgePath = Paths.get(gpioPinDirectory + "/edge"); } valuePath = Paths.get(gpioPinDirectory + "/value"); this.debounceInterval = debounceInterval; } /** * Stops all spawned pin threads. * * @throws IOException if can't obtain pin lock in timely fashion * or was interrupted while waiting for lock */ public void stopEventProcessing() throws IOException { try { if (pinLock.writeLock().tryLock(PINLOCK_TIMEOUT, PINLOCK_TIMEOUT_UNITS)) { try { if (eventListenerThread != null) { eventHandlers = null; eventListenerThread.interrupt(); eventListenerThread = null; /* Give some time to event listener thread to notice the interrupt and do cleanup */ Thread.sleep(EventListener.POLL_TIMEOUT * 2); } } catch (InterruptedException e) { throw new IOException( "The thread was interrupted while waiting for GPIO pin event listener thread to finish"); } finally { pinLock.writeLock().unlock(); } } else { /* * Something wrong happened, throw an exception and move on or we are risking to block the whole system */ throw new IOException("Write GPIO pin lock can't be aquired for " + PINLOCK_TIMEOUT + " " + PINLOCK_TIMEOUT_UNITS.toString()); } } catch (InterruptedException e) { throw new IOException("The thread was interrupted while waiting for write GPIO pin lock"); } } public int getActiveLow() throws IOException { int activeLow; try { if (pinLock.readLock().tryLock(PINLOCK_TIMEOUT, PINLOCK_TIMEOUT_UNITS)) { try { /* Gets the value of first line from 'activelow' file parsed to integer. */ activeLow = Integer.parseInt(Files.readAllLines(activelowPath, DEFAULT_ENCODING).get(0)); } catch (NumberFormatException e) { throw new IOException("Unsupported, not numeric 'activelow' value", e); } finally { pinLock.readLock().unlock(); } if ((activeLow != GPIOPin.ACTIVELOW_DISABLED) && (activeLow != GPIOPin.ACTIVELOW_ENABLED)) { throw new IOException("Unsupported 'activelow' value (" + activeLow + ")"); } } else { /* * Something wrong happened, throw an exception and move on or we are risking to block the whole system */ throw new IOException("Read GPIO pin lock can't be aquired for " + PINLOCK_TIMEOUT + " " + PINLOCK_TIMEOUT_UNITS.toString()); } } catch (InterruptedException e) { throw new IOException("The thread was interrupted while waiting for read GPIO pin lock"); } return activeLow; } public void setActiveLow(Integer activeLow) throws IOException { if ((activeLow != GPIOPin.ACTIVELOW_DISABLED) && (activeLow != GPIOPin.ACTIVELOW_ENABLED)) { throw new IllegalArgumentException("Unsupported argument for 'activeLow' parameter (" + activeLow + ")"); } try { if (pinLock.readLock().tryLock(PINLOCK_TIMEOUT, PINLOCK_TIMEOUT_UNITS)) { try { Files.write(activelowPath, activeLow.toString().getBytes()); } finally { pinLock.readLock().unlock(); } } else { /* * Something wrong happened, throw an exception and move on or we are risking to block the whole system */ throw new IOException("Read GPIO pin lock can't be aquired for " + PINLOCK_TIMEOUT + " " + PINLOCK_TIMEOUT_UNITS.toString()); } } catch (InterruptedException e) { throw new IOException("The thread was interrupted while waiting for read GPIO pin lock"); } } public int getDirection() throws IOException { String direction; try { if (pinLock.readLock().tryLock(PINLOCK_TIMEOUT, PINLOCK_TIMEOUT_UNITS)) { try { /* The board/pin may not support direction change */ if (directionPath == null) { throw new IOException("The pin doesn't support 'get direction' operation"); } /* Read first line from 'direction' file */ direction = Files.readAllLines(directionPath, DEFAULT_ENCODING).get(0); } finally { pinLock.readLock().unlock(); } } else { /* * Something wrong happened, throw an exception and move on or we are risking to block the whole system */ throw new IOException("Read GPIO pin lock can't be aquired for " + PINLOCK_TIMEOUT + " " + PINLOCK_TIMEOUT_UNITS.toString()); } } catch (InterruptedException e) { throw new IOException("The thread was interrupted while waiting for read GPIO pin lock"); } if (direction.compareToIgnoreCase("in") == 0) { return GPIOPin.DIRECTION_IN; } else { if (direction.compareToIgnoreCase("out") == 0) { return GPIOPin.DIRECTION_OUT; } else { if (direction.compareToIgnoreCase("high") == 0) { return GPIOPin.DIRECTION_OUT_HIGH; } else { if (direction.compareToIgnoreCase("low") == 0) { return GPIOPin.DIRECTION_OUT_LOW; } else { throw new IOException("Unsupported 'direction' value (" + direction + ")"); } } } } } public void setDirection(int direction) throws IOException { String newDirection; switch (direction) { case GPIOPin.DIRECTION_IN: newDirection = "in"; break; case GPIOPin.DIRECTION_OUT: newDirection = "out"; break; case GPIOPin.DIRECTION_OUT_HIGH: newDirection = "high"; break; case GPIOPin.DIRECTION_OUT_LOW: newDirection = "low"; break; default: throw new IllegalArgumentException( "Unsupported argument for 'direction' parameter (" + direction + ")"); } try { if (pinLock.readLock().tryLock(PINLOCK_TIMEOUT, PINLOCK_TIMEOUT_UNITS)) { try { /* The board/pin may not support direction change */ if (directionPath == null) { throw new IOException("The pin doesn't support 'set direction' operation"); } Files.write(directionPath, newDirection.getBytes()); } finally { pinLock.readLock().unlock(); } } else { /* * Something wrong happened, throw an exception and move on or we are risking to block the whole system */ throw new IOException("Read GPIO pin lock can't be aquired for " + PINLOCK_TIMEOUT + " " + PINLOCK_TIMEOUT_UNITS.toString()); } } catch (InterruptedException e) { throw new IOException("The thread was interrupted while waiting for read GPIO pin lock"); } } public int getEdgeDetection() throws IOException { String edgeDetection; try { if (pinLock.readLock().tryLock(PINLOCK_TIMEOUT, PINLOCK_TIMEOUT_UNITS)) { try { /* The board/pin may not support interrupts */ if (edgePath == null) { throw new IOException("The pin doesn't support 'get edge detection' operation"); } edgeDetection = Files.readAllLines(edgePath, DEFAULT_ENCODING).get(0); } finally { pinLock.readLock().unlock(); } } else { /* * Something wrong happened, throw an exception and move on or we are risking to block the whole system */ throw new IOException("Read GPIO pin lock can't be aquired for " + PINLOCK_TIMEOUT + " " + PINLOCK_TIMEOUT_UNITS.toString()); } } catch (InterruptedException e) { throw new IOException("The thread was interrupted while waiting for read GPIO pin lock"); } if (edgeDetection.compareToIgnoreCase("none") == 0) { return GPIOPin.EDGEDETECTION_NONE; } else { if (edgeDetection.compareToIgnoreCase("rising") == 0) { return GPIOPin.EDGEDETECTION_RISING; } else { if (edgeDetection.compareToIgnoreCase("falling") == 0) { return GPIOPin.EDGEDETECTION_FALLING; } else { if (edgeDetection.compareToIgnoreCase("both") == 0) { return GPIOPin.EDGEDETECTION_BOTH; } else { throw new IOException("Unsupported 'edge detection' value (" + edgeDetection + ")"); } } } } } public void setEdgeDetection(int edgeDetection) throws IOException { String newEdgeDetection; switch (edgeDetection) { case GPIOPin.EDGEDETECTION_NONE: newEdgeDetection = "none"; break; case GPIOPin.EDGEDETECTION_RISING: newEdgeDetection = "rising"; break; case GPIOPin.EDGEDETECTION_FALLING: newEdgeDetection = "falling"; break; case GPIOPin.EDGEDETECTION_BOTH: newEdgeDetection = "both"; break; default: throw new IllegalArgumentException( "Unsupported argument for 'edge detection' parameter (" + edgeDetection + ")"); } try { if (pinLock.readLock().tryLock(PINLOCK_TIMEOUT, PINLOCK_TIMEOUT_UNITS)) { try { /* The board/pin may not support interrupts */ if (edgePath == null) { throw new IOException("The pin doesn't support 'set edge detection' operation"); } Files.write(edgePath, newEdgeDetection.getBytes()); } finally { pinLock.readLock().unlock(); } } else { /* * Something wrong happened, throw an exception and move on or we are risking to block the whole system */ throw new IOException("Read GPIO pin lock can't be aquired for " + PINLOCK_TIMEOUT + " " + PINLOCK_TIMEOUT_UNITS.toString()); } } catch (InterruptedException e) { throw new IOException("The thread was interrupted while waiting for read GPIO pin lock"); } } public int getValue() throws IOException { int value; try { if (pinLock.readLock().tryLock(PINLOCK_TIMEOUT, PINLOCK_TIMEOUT_UNITS)) { try { /* Gets the value of first line from 'value' file parsed to integer. */ value = Integer.parseInt(Files.readAllLines(valuePath, DEFAULT_ENCODING).get(0)); } catch (NumberFormatException e) { throw new IOException("Unsupported, not numeric 'value' value", e); } finally { pinLock.readLock().unlock(); } } else { /* * Something wrong happened, throw an exception and move on or we are risking to block the whole system */ throw new IOException("Read GPIO pin lock can't be aquired for " + PINLOCK_TIMEOUT + " " + PINLOCK_TIMEOUT_UNITS.toString()); } } catch (InterruptedException e) { throw new IOException("The thread was interrupted while waiting for read GPIO pin lock"); } if ((value != GPIOPin.VALUE_LOW) && (value != GPIOPin.VALUE_HIGH)) { throw new IOException("Unsupported 'value' value (" + value + ")"); } return value; } public void setValue(Integer value) throws IOException { if ((value != GPIOPin.VALUE_LOW) && (value != GPIOPin.VALUE_HIGH)) { throw new IllegalArgumentException("Unsupported argument for 'value' parameter (" + value + ")"); } try { if (pinLock.readLock().tryLock(PINLOCK_TIMEOUT, PINLOCK_TIMEOUT_UNITS)) { try { Files.write(valuePath, value.toString().getBytes()); } finally { pinLock.readLock().unlock(); } } else { /* * Something wrong happened, throw an exception and move on or we are risking to block the whole system */ throw new IOException("Read GPIO pin lock can't be aquired for " + PINLOCK_TIMEOUT + " " + PINLOCK_TIMEOUT_UNITS.toString()); } } catch (InterruptedException e) { throw new IOException("The thread was interrupted while waiting for read GPIO pin lock"); } } public int getPinNumber() { return pinNumber; } public long getDebounceInterval() { return debounceInterval; } public void setDebounceInterval(long debounceInterval) throws IOException { if (debounceInterval < 0) { throw new IllegalArgumentException( "Unsupported argument for 'debounceInterval' parameter (" + debounceInterval + ")"); } try { if (pinLock.writeLock().tryLock(PINLOCK_TIMEOUT, PINLOCK_TIMEOUT_UNITS)) { try { this.debounceInterval = debounceInterval; } finally { pinLock.writeLock().unlock(); } } else { /* * Something wrong happened, throw an exception and move on or we are risking to block the whole system */ throw new IOException("Write GPIO pin lock can't be aquired for " + PINLOCK_TIMEOUT + " " + PINLOCK_TIMEOUT_UNITS.toString()); } } catch (InterruptedException e) { throw new IOException("The thread was interrupted while waiting for write GPIO pin lock"); } } public void addEventHandler(GPIOPinEventHandler eventHandler) throws IOException { if (eventHandler == null) { throw new IllegalArgumentException("Unsupported argument for 'eventHandler' parameter (null)"); } try { if (pinLock.writeLock().tryLock(PINLOCK_TIMEOUT, PINLOCK_TIMEOUT_UNITS)) { try { /* The board/pin may not support interrupts */ if (edgePath == null) { throw new IOException("Interrupts aren't supported for this pin"); } if (!eventHandlers.add(eventHandler)) { throw new IllegalArgumentException("The event handler is already registered"); } /* Start event listener thread if not running */ if (eventListenerThread == null) { eventListenerThread = new EventListener(this); eventListenerThread.setName("openHAB GPIO event listener (pin " + pinNumber + ")"); eventListenerThread.start(); } } finally { pinLock.writeLock().unlock(); } } else { /* * Something wrong happened, throw an exception and move on or we are risking to block the whole system */ throw new IOException("Write GPIO pin lock can't be aquired for " + PINLOCK_TIMEOUT + " " + PINLOCK_TIMEOUT_UNITS.toString()); } } catch (InterruptedException e) { throw new IOException("The thread was interrupted while waiting for write GPIO pin lock"); } } public void removeEventHandler(GPIOPinEventHandler eventHandler) throws IOException { if (eventHandler == null) { throw new IllegalArgumentException("Unsupported argument for 'eventHandler' parameter (null)"); } try { if (pinLock.writeLock().tryLock(PINLOCK_TIMEOUT, PINLOCK_TIMEOUT_UNITS)) { try { if (!eventHandlers.remove(eventHandler)) { throw new IllegalArgumentException("The event handler isn't registered"); } /* Stop event listener thread if there are no other registered handlers */ if (eventHandlers.isEmpty()) { eventListenerThread.interrupt(); eventListenerThread = null; } } finally { pinLock.writeLock().unlock(); } } else { /* * Something wrong happened, throw an exception and move on or we are risking to block the whole system */ throw new IOException("Write GPIO pin lock can't be aquired for " + PINLOCK_TIMEOUT + " " + PINLOCK_TIMEOUT_UNITS.toString()); } } catch (InterruptedException e) { throw new IOException("The thread was interrupted while waiting for write GPIO pin lock"); } } /** * Event listener thread, listens for interrupts and executes registered * handlers each in its own thread. * * @author Dancho Penev * @since 1.5.0 */ private class EventListener extends Thread { private static final int POLL_TIMEOUT = 1000; /** Pin object for which events are listened. */ private GPIOPinLinux pin; public EventListener(GPIOPinLinux pin) { this.pin = pin; } @Override public void run() { /* File descriptor for 'value' file */ int fd = -1; /* Thread pool for event handlers */ ExecutorService executorService = Executors.newCachedThreadPool(); try { int rc; pollfd[] pollfdset = { new pollfd() }; ByteByReference value = new ByteByReference(); NativeLong zero = new NativeLong(0); /* Last time (in milliseconds) when the interrupt was generated */ long lastInterruptTime = 0; fd = LibC.INSTANCE.open(pin.valuePath.toString(), LibC.O_RDONLY | LibC.O_NONBLOCK); pollfdset[0].fd = fd; pollfdset[0].events = LibC.POLLPRI; /* * Prior calling poll() the file needs to be read or poll() will return immediately without real * interrupt received */ try { LibC.INSTANCE.read(pollfdset[0].fd, value.getPointer(), 1); } catch (LastErrorException e) { } while (!interrupted()) { /* * Wait for GPIO interrupt or timeout to occur. Timeouts provides possibility to check thread * interrupt status */ rc = LibC.INSTANCE.poll(pollfdset, 1, POLL_TIMEOUT); switch (rc) { /* Timeout, poll() again */ case 0: continue; /* There is one file descriptor ready */ case 1: /* Is interrupt received? */ if ((pollfdset[0].revents & LibC.POLLPRI) > 0) { /* Calculate times for software debounce */ long interruptTime = System.currentTimeMillis(); long timeDifference = interruptTime - lastInterruptTime; /* Go to file start and read first byte */ LibC.INSTANCE.lseek(fd, zero, LibC.SEEK_SET); rc = LibC.INSTANCE.read(pollfdset[0].fd, value.getPointer(), 1); /* There is exactly one byte read */ if (rc == 1) { /* Execute event handlers each in its own thread */ try { if (pinLock.readLock().tryLock(PINLOCK_TIMEOUT, PINLOCK_TIMEOUT_UNITS)) { try { /* Software debounce */ if ((timeDifference > pin.debounceInterval) || (timeDifference < 0)) { for (GPIOPinEventHandler eventHandler : pin.eventHandlers) { EventHandlerExecutor eventHandlerExecutor = new EventHandlerExecutor( pin, eventHandler, Character.getNumericValue(value.getValue())); executorService.execute(eventHandlerExecutor); } lastInterruptTime = interruptTime; } } finally { pin.pinLock.readLock().unlock(); } } else { /* * Something wrong happened, throw an exception and move on or we are * risking to block the whole system */ throw new IOException("Read GPIO pin lock can't be aquired for " + PINLOCK_TIMEOUT + " " + PINLOCK_TIMEOUT_UNITS.toString()); } } catch (InterruptedException e) { throw new IOException( "The thread was interrupted while waiting for read GPIO pin lock"); } /* poll() again */ continue; } else { throw new IOException( "Unsupported return value from native 'read' function (" + rc + ")"); } } break; default: throw new IOException("Unsupported return value from native 'poll' function (" + rc + ")"); } } } catch (Exception e) { /* Execute error handlers each in its own thread */ try { if (pinLock.readLock().tryLock(PINLOCK_TIMEOUT, PINLOCK_TIMEOUT_UNITS)) { try { for (GPIOPinEventHandler eventHandler : pin.eventHandlers) { EventHandlerExecutor eventHandlerExecutor = new EventHandlerExecutor(pin, eventHandler, e); executorService.execute(eventHandlerExecutor); } } finally { pin.pinLock.readLock().unlock(); } } } catch (InterruptedException ex) { } } finally { /* Cleanup */ LibC.INSTANCE.close(fd); executorService.shutdown(); } } /** * Executes callback functions. * * @author Dancho Penev * @since 1.5.0 */ private class EventHandlerExecutor implements Runnable { private static final int TYPE_EVENT = 0; private static final int TYPE_ERROR = 1; private int type; private GPIOPinLinux pin; private GPIOPinEventHandler eventHandler; private int value; private Exception exception; public EventHandlerExecutor(GPIOPinLinux pin, GPIOPinEventHandler eventHandler, int value) { this.type = TYPE_EVENT; this.pin = pin; this.eventHandler = eventHandler; this.value = value; } public EventHandlerExecutor(GPIOPinLinux pin, GPIOPinEventHandler eventHandler, Exception exception) { this.type = TYPE_ERROR; this.pin = pin; this.eventHandler = eventHandler; this.exception = exception; } public void run() { switch (type) { case TYPE_EVENT: eventHandler.onEvent(pin, value); break; case TYPE_ERROR: eventHandler.onError(pin, exception); break; } } } } }