/* -*- mode: java; c-basic-offset: 2; indent-tabs-mode: nil -*- */ /* Copyright (c) The Processing Foundation 2015 Hardware I/O library developed by Gottfried Haider as part of GSoC 2015 This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ package processing.io; import processing.core.*; import processing.io.NativeInterface; import java.lang.reflect.Method; import java.util.BitSet; import java.util.HashMap; import java.util.Map; /** * @webref */ public class GPIO { // those constants are generally the same as in Arduino.h public static final int INPUT = 0; public static final int OUTPUT = 1; public static final int INPUT_PULLUP = 2; public static final int INPUT_PULLDOWN = 3; public static final int LOW = 0; public static final int HIGH = 1; public static final int NONE = 0; /** * trigger when level changes */ public static final int CHANGE = 1; /** * trigger when level changes from high to low */ public static final int FALLING = 2; /** * trigger when level changes from low to high */ public static final int RISING = 3; protected static Map<Integer, Thread> irqThreads = new HashMap<Integer, Thread>(); protected static boolean serveInterrupts = true; protected static BitSet values = new BitSet(); static { NativeInterface.loadLibrary(); } public static void analogWrite(int pin, int value) { // currently this can't be done in a non-platform-specific way // the best way forward would be implementing a generic, "soft" // PWM in the kernel that uses high resolution timers, similiar // to the patch Bill Gatliff posted, which unfortunately didn't // get picked up, see // https://dev.openwrt.org/browser/trunk/target/linux/generic/files/drivers/pwm/gpio-pwm.c?rev=35328 // additionally, there currently doesn't seem to be a way to link // a PWM channel back to the GPIO pin it is associated with // alternatively, this could be implemented in user-space to some // degree // see http://stackoverflow.com/a/13371570/3030124 // see http://raspberrypi.stackexchange.com/a/304 throw new RuntimeException("Not yet implemented"); } /** * Calls a function when the value of an input pin changes * @param pin GPIO pin * @param parent typically use "this" * @param method name of sketch method to call * @param mode when to call: GPIO.CHANGE, GPIO.FALLING or GPIO.RISING * @see noInterrupts * @see interrupts * @see releaseInterrupt * @webref */ public static void attachInterrupt(int pin, PApplet parent, String method, int mode) { if (irqThreads.containsKey(pin)) { throw new RuntimeException("You must call releaseInterrupt before attaching another interrupt on the same pin"); } enableInterrupt(pin, mode); final int irqPin = pin; final PApplet irqObject = parent; final Method irqMethod; try { irqMethod = parent.getClass().getMethod(method, int.class); } catch (NoSuchMethodException e) { throw new RuntimeException("Method " + method + " does not exist"); } // it might be worth checking how Java threads compare to pthreads in terms // of latency Thread t = new Thread(new Runnable() { public void run() { boolean gotInterrupt = false; try { do { try { if (waitForInterrupt(irqPin, 100)) { gotInterrupt = true; } if (gotInterrupt && serveInterrupts) { irqMethod.invoke(irqObject, irqPin); gotInterrupt = false; } // if we received an interrupt while interrupts were disabled // we still deliver it the next time interrupts get enabled // not sure if everyone agrees with this logic though } catch (RuntimeException e) { // make sure we're not busy spinning on error Thread.sleep(100); } } while (!Thread.currentThread().isInterrupted()); } catch (Exception e) { // terminate the thread on any unexpected exception that might occur System.err.println("Terminating interrupt handling for pin " + irqPin + " after catching: " + e.getMessage()); } } }, "GPIO" + pin + " IRQ"); t.setPriority(Thread.MAX_PRIORITY); t.start(); irqThreads.put(pin, t); } /** * Checks if the GPIO pin number can be valid * * Board-specific classes, such as RPI, assign -1 to pins that carry power, * ground and the like. * @param pin GPIO pin */ protected static void checkValidPin(int pin) { if (pin < 0) { throw new RuntimeException("Operation not supported on this pin"); } } /** * Returns the value of an input pin * @param pin GPIO pin * @return GPIO.HIGH (1) or GPIO.LOW (0) * @see pinMode * @see digitalWrite * @webref */ public static int digitalRead(int pin) { checkValidPin(pin); if (NativeInterface.isSimulated()) { return LOW; } String fn = String.format("/sys/class/gpio/gpio%d/value", pin); byte in[] = new byte[2]; int ret = NativeInterface.readFile(fn, in); if (ret < 0) { throw new RuntimeException(NativeInterface.getError(ret)); } else if (1 <= ret && in[0] == '0') { return LOW; } else if (1 <= ret && in[0] == '1') { return HIGH; } else { System.err.print("Read " + ret + " bytes"); if (0 < ret) { System.err.format(", first byte is 0x%02x" + in[0]); } System.err.println(); throw new RuntimeException("Unexpected value"); } } /** * Sets an output pin to be either high or low * @param pin GPIO pin * @param value GPIO.HIGH (1) or GPIO.LOW (0) * @see pinMode * @see digitalRead * @webref */ public static void digitalWrite(int pin, int value) { checkValidPin(pin); String out; if (value == LOW) { // values are also stored in a bitmap to make it possible to set a // default level per pin before enabling the output values.clear(pin); out = "0"; } else if (value == HIGH) { values.set(pin); out = "1"; } else { System.err.println("Only GPIO.LOW and GPIO.HIGH, 0 and 1, or true and false, can be used."); throw new IllegalArgumentException("Illegal value"); } if (NativeInterface.isSimulated()) { return; } String fn = String.format("/sys/class/gpio/gpio%d/value", pin); int ret = NativeInterface.writeFile(fn, out); if (ret < 0) { if (ret != -2) { // ENOENT, pin might not yet be exported throw new RuntimeException(NativeInterface.getError(ret)); } } } /** * @param value true or false */ public static void digitalWrite(int pin, boolean value) { if (value) { digitalWrite(pin, HIGH); } else { digitalWrite(pin, LOW); } } /** * Disables an interrupt for an input pin * @param pin GPIO pin * @see enableInterrupt * @see waitForInterrupt */ protected static void disableInterrupt(int pin) { enableInterrupt(pin, NONE); } /** * Enables an interrupt for an input pin * @param pin GPIO pin * @param mode what to wait for: GPIO.CHANGE, GPIO.FALLING or GPIO.RISING * @see waitForInterrupt * @see disableInterrupt */ protected static void enableInterrupt(int pin, int mode) { checkValidPin(pin); String out; if (mode == NONE) { out = "none"; } else if (mode == CHANGE) { out = "both"; } else if (mode == FALLING) { out = "falling"; } else if (mode == RISING) { out = "rising"; } else { throw new IllegalArgumentException("Unknown mode"); } if (NativeInterface.isSimulated()) { return; } String fn = String.format("/sys/class/gpio/gpio%d/edge", pin); int ret = NativeInterface.writeFile(fn, out); if (ret < 0) { if (ret == -2) { // ENOENT System.err.println("Make sure your called pinMode on the input pin"); } throw new RuntimeException(NativeInterface.getError(ret)); } } /** * Allows interrupts to happen * @see attachInterrupt * @see noInterrupts * @see releaseInterrupt * @webref */ public static void interrupts() { serveInterrupts = true; } /** * Prevents interrupts from happpening * @see attachInterrupt * @see interrupts * @see releaseInterrupt * @webref */ public static void noInterrupts() { serveInterrupts = false; } /** * Configures a pin to act either as input or output * @param pin GPIO pin * @param mode GPIO.INPUT or GPIO.OUTPUT * @see digitalRead * @see digitalWrite * @see releasePin * @webref */ public static void pinMode(int pin, int mode) { checkValidPin(pin); if (NativeInterface.isSimulated()) { return; } // export pin through sysfs String fn = "/sys/class/gpio/export"; int ret = NativeInterface.writeFile(fn, Integer.toString(pin)); if (ret < 0) { if (ret == -2) { // ENOENT System.err.println("Make sure your kernel is compiled with GPIO_SYSFS enabled"); } if (ret == -22) { // EINVAL System.err.println("GPIO pin " + pin + " does not seem to be available on your platform"); } if (ret != -16) { // EBUSY, returned when the pin is already exported throw new RuntimeException(fn + ": " + NativeInterface.getError(ret)); } } // delay to give udev a chance to change the file permissions behind our back // there should really be a cleaner way for this try { Thread.sleep(500); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } // set direction and default level for outputs fn = String.format("/sys/class/gpio/gpio%d/direction", pin); String out; if (mode == INPUT) { out = "in"; } else if (mode == OUTPUT) { if (values.get(pin)) { out = "high"; } else { out = "low"; } } else if (mode == INPUT_PULLUP || mode == INPUT_PULLDOWN) { // currently this can't be done in a non-platform-specific way, see // http://lists.infradead.org/pipermail/linux-rpi-kernel/2015-August/002146.html throw new RuntimeException("Not yet implemented"); } else { throw new IllegalArgumentException("Unknown mode"); } ret = NativeInterface.writeFile(fn, out); if (ret < 0) { throw new RuntimeException(fn + ": " + NativeInterface.getError(ret)); } } /** * Stops listening for interrupts on an input pin * @param pin GPIO pin * @see attachInterrupt * @see noInterrupts * @see interrupts * @webref */ public static void releaseInterrupt(int pin) { Thread t = irqThreads.get(pin); if (t == null) { return; } t.interrupt(); try { t.join(); } catch (InterruptedException e) { System.err.println("Error joining thread in releaseInterrupt: " + e.getMessage()); } t = null; irqThreads.remove(pin); disableInterrupt(pin); } /** * Gives ownership of a pin back to the operating system * @param pin GPIO pin * @see pinMode * @webref */ public static void releasePin(int pin) { checkValidPin(pin); if (NativeInterface.isSimulated()) { return; } String fn = "/sys/class/gpio/unexport"; int ret = NativeInterface.writeFile(fn, Integer.toString(pin)); if (ret < 0) { if (ret == -2) { // ENOENT System.err.println("Make sure your kernel is compiled with GPIO_SYSFS enabled"); } // EINVAL is returned when trying to unexport pins that weren't exported to begin with, ignore this case if (ret != -22) { throw new RuntimeException(NativeInterface.getError(ret)); } } } /** * Waits for the value of an input pin to change * @param pin GPIO pin * @param mode what to wait for: GPIO.CHANGE, GPIO.FALLING or GPIO.RISING * @webref */ public static void waitFor(int pin, int mode) { waitFor(pin, mode, -1); } /** * Waits for the value of an input pin to change * * This function will throw a RuntimeException in case of a timeout. * @param timeout don't wait more than timeout milliseconds * @webref */ public static void waitFor(int pin, int mode, int timeout) { enableInterrupt(pin, mode); if (waitForInterrupt(pin, timeout) == false) { throw new RuntimeException("Timeout occurred"); } } public static boolean waitForInterrupt(int pin, int mode, int timeout) { throw new RuntimeException("The waitForInterrupt function has been renamed to waitFor. Please update your sketch accordingly."); } /** * Waits for the value of an input pin to change * * Make sure to setup the interrupt with enableInterrupt() before calling * this function. A timeout value of -1 waits indefinitely. * @param pin GPIO pin * @param timeout don't wait more than timeout milliseconds * @return true if the interrupt occured, false if the timeout occured * @see enableInterrupt * @see disableInterrupt */ protected static boolean waitForInterrupt(int pin, int timeout) { checkValidPin(pin); if (NativeInterface.isSimulated()) { // pretend the interrupt happens after 200ms try { Thread.sleep(200); } catch (InterruptedException e) {} return true; } String fn = String.format("/sys/class/gpio/gpio%d/value", pin); int ret = NativeInterface.pollDevice(fn, timeout); if (ret < 0) { if (ret == -2) { // ENOENT System.err.println("Make sure your called pinMode on the input pin"); } throw new RuntimeException(NativeInterface.getError(ret)); } else if (ret == 0) { // timeout return false; } else { // interrupt return true; } } }