diff --git a/.gitignore b/.gitignore index 34c8bf8..ad311ba 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,6 @@ target/ # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml hs_err_pid* + +# Intellij Files +.idea \ No newline at end of file diff --git a/src/main/java/ch/eitchnet/beaglebone/GpioBridge.java b/src/main/java/ch/eitchnet/beaglebone/GpioBridge.java index f5882c4..a0a5431 100644 --- a/src/main/java/ch/eitchnet/beaglebone/GpioBridge.java +++ b/src/main/java/ch/eitchnet/beaglebone/GpioBridge.java @@ -1,400 +1,96 @@ package ch.eitchnet.beaglebone; -import java.io.BufferedReader; -import java.io.File; -import java.io.FileNotFoundException; -import java.io.FileOutputStream; -import java.io.FileReader; -import java.io.IOException; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -/** - *

- * Main object to give access to GPIO ports on a Linux kernel - *

- * - *

- * The {@link GpioBridge} is a singleton. Features include retrieving Pins, writing and reading values, as well as - * registering observers for changes to input pins - *

- * - *

- * {@link Gpio} objects are cached and their {@link Signal} is set by the {@link GpioBridge} accordingly - *

- * - * @author Robert von Burg <eitch@eitchnet.ch> - */ -public class GpioBridge { - - private static final String GPIO_PATH = "/sys/class/gpio/"; - - private Map cache; - private Map> listeners; - private Thread thread; - private volatile boolean run; - - private static final GpioBridge instance; - - static { - instance = new GpioBridge(); - } - - /** - * @return the instance of the {@link GpioBridge} - */ - public static GpioBridge getInstance() { - return instance; - } - - /** - * Private constructor for singleton construction - */ - private GpioBridge() { - this.cache = new HashMap<>(); - this.listeners = Collections.synchronizedMap(new HashMap<>()); - } - - /** - * Returns the kernel file path to the value of the {@link Gpio} - * - * @param gpio - * the {@link Gpio} for which the path is to be returned - * - * @return the Path to the {@link Gpio}'s value - */ - private File getGpioValuePath(Gpio gpio) { - return new File(GPIO_PATH, gpio.getKernelName() + "/value"); - } - - /** - * Returns the kernel file path to the direction of the {@link Gpio} - * - * @param gpio - * the {@link Gpio} for which the path is to be returned - * - * @return the Path to the {@link Gpio}'s direction - */ - private File getGpioDirectionPath(Gpio gpio) { - return new File(GPIO_PATH, gpio.getKernelName() + "/direction"); - } - +public interface GpioBridge { /** *

* Public API method to write the given {@link Signal} on the given {@link Gpio}'s pin. *

- * + * * @param gpio * the {@link Gpio} to which the {@link Signal} should be written * @param signal * the {@link Signal} to write to the given {@link Gpio} - * + * * @throws GpioException * if the direction of the {@link Gpio} is not {@link Direction#OUT}, or if something goes wrong while * writing to the file */ - public void writeValue(Gpio gpio, Signal signal) throws GpioException { - - synchronized (gpio) { - if (gpio.getDirection() != Direction.OUT) - throw new GpioException("For writing the direction must be " + Direction.OUT); - - File file = getGpioValuePath(gpio); - try (FileOutputStream out = new FileOutputStream(file)) { - - out.write(signal.getValueS().getBytes()); - out.flush(); - - gpio.setSignal(signal); - - } catch (Exception e) { - throw new GpioException("Failed to write GPIO " + gpio + " with signal " + signal, e); - } - - System.out.println("Set GPIO " + gpio.getPin() + " signal to " + gpio.getSignal()); - } - } + void writeValue(Gpio gpio, Signal signal) throws GpioException; /** *

* Public API method to read the current {@link Signal} on the given {@link Gpio}'s pin. *

- * + * * @param gpio * the {@link Gpio} for which the {@link Signal} should be read - * + * * @return The {@link Gpio}'s current signal - * + * * @throws GpioException * if the direction of the {@link Gpio} is not {@link Direction#IN}, or if something goes wrong while * reading from the file */ - public Signal readValue(Gpio gpio) throws GpioException { - - synchronized (gpio) { - - if (gpio.getDirection() != Direction.IN) - throw new GpioException("For reading the direction must be " + Direction.IN); - - File file = getGpioValuePath(gpio); - try (BufferedReader fin = new BufferedReader(new FileReader(file))) { - - String valueS = fin.readLine(); - Signal signal = Signal.getSignal(valueS); - if (!gpio.getSignal().equals(signal)) - gpio.setSignal(signal); - - return signal; - - } catch (Exception e) { - throw new GpioException("Failed to read GPIO " + gpio, e); - } - } - } + Signal readValue(Gpio gpio) throws GpioException; /** * Starts the {@link GpioBridge}'s signal observing {@link Thread}. If no observers are registered with the * {@link #register(Gpio, GpioSignalListener)}-method, then this method needs not to be called. */ - public void start() { - - this.run = true; - this.thread = new Thread(() -> { - while (this.run) { - if (this.listeners.isEmpty()) { - synchronized (this) { - try { - wait(1000l); - } catch (InterruptedException e) { - System.out.println("Was interrupted. Stopping thread."); - this.run = false; - break; - } - } - } else { - - List changes = new ArrayList<>(); - - synchronized (this.listeners) { - for (Gpio gpio : this.listeners.keySet()) { - try { - synchronized (gpio) { - Signal currentSignal = gpio.getSignal(); - Signal newSignal = readValue(gpio); - if (currentSignal != newSignal) - changes.add(gpio); - } - } catch (Exception e) { - System.out.println("Failed to read GPIO " + gpio + " due to:"); - e.printStackTrace(); - this.run = false; - break; - } - } - } - - if (!changes.isEmpty()) - System.out.println("Found " + changes.size() + " GPIO changes."); - - for (Gpio gpio : changes) { - synchronized (this.listeners) { - List listeners = this.listeners.get(gpio); - System.out.println("GPIO " + gpio.getPin() + " changed to " + gpio.getSignal() - + ". Notifying " + listeners.size() + " listeners."); - - for (GpioSignalListener listener : listeners) { - try { - listener.notify(gpio); - } catch (Exception e) { - System.out.println("Failed to update listener " + listener + " due to:"); - e.printStackTrace(); - } - } - } - } - - try { - Thread.sleep(200l); - } catch (InterruptedException e) { - System.out.println("Was interrupted. Stopping thread."); - this.run = false; - break; - } - } - } - }, "gpio_reader"); - this.thread.start(); - System.out.println("Started GPIO bridge."); - } + void start(); /** * Stops observing any pins and stops the {@link Thread} */ - public void stop() { - this.run = false; - this.thread.interrupt(); - try { - this.thread.join(5000l); - } catch (InterruptedException e) { - System.out.println("Was interrupted while waiting for thread to stop?!"); - } - } + void stop(); /** *

* Returns the {@link Gpio} for the given {@link Direction}. *

- * + * *

* Note: This method can not be called multiple times with different {@link Direction}s. The * {@link GpioBridge} does not handle pins that are simultaneously input and output as this is not supported by the * Linux kernel. *

- * + * * @param pin * The {@link Pin} for which the {@link Gpio} in the given {@link Direction} is to be returned * @param direction * the {@link Direction} for which this {@link Gpio} is to be returned - * + * * @return The {@link Gpio} with the configured {@link Direction} - * + * * @throws GpioException * If the given {@link Direction} does not match the kernel's configured direction, or if the file * permissions are not set so that the Java process can access the file (read access for input pin, * write access for output pin. */ - public synchronized Gpio getGpio(Pin pin, Direction direction) throws GpioException { - Gpio gpio = this.cache.get(pin); - if (gpio == null) { - - gpio = new Gpio(pin, direction); - - // validate direction - assertDirection(gpio); - - // validate file permissions - validateFilePermissions(gpio); - - this.cache.put(pin, gpio); - System.out.println("Initialized pin " + pin + " with direction " + direction + "."); - } - - return gpio; - } - - /** - * Validates the direction of the {@link Gpio} is the same as kernel's exported state - * - * @param gpio - * the {@link Gpio} been asserted for direction - * - * @throws GpioException - * if the assertion fails - */ - private void assertDirection(Gpio gpio) throws GpioException { - File file = getGpioDirectionPath(gpio); - Pin pin = gpio.getPin(); - try (BufferedReader fin = new BufferedReader(new FileReader(file))) { - - String directionS = fin.readLine(); - Direction dir = Direction.getDirection(directionS); - if (dir != gpio.getDirection()) - throw new GpioException("Actual direction of GPIO " + pin + " is " + dir + " not " + directionS); - - } catch (FileNotFoundException e) { - throw new GpioException("GPIO " + pin + " does not exist, was the pin exported to user space?", e); - } catch (IOException e) { - throw new GpioException("Failed to open GPIO " + pin, e); - } - } - - /** - * Validates the file permissions of the {@link Gpio} is correct for the {@link Gpio}'s {@link Direction}: - *
    - *
  • read access for input pin
  • - *
  • write access for output pin
  • - *
- * - * @param gpio - * the {@link Gpio} been asserted for direction - * - * @throws GpioException - * if the assertion fails - */ - private void validateFilePermissions(Gpio gpio) throws GpioException { - File gpioValuePath = getGpioValuePath(gpio); - Direction direction = gpio.getDirection(); - if (direction == Direction.IN) { - if (!gpioValuePath.canRead()) - throw new GpioException("GPIO " + gpio + " has direction " + direction - + " and is not readable. Are the file permissions ok?"); - - } else if (direction == Direction.OUT) { - if (!gpioValuePath.canWrite()) - throw new GpioException("GPIO " + gpio + " has direction " + direction - + " and is not writable. Are the file permissions ok?"); - } else { - throw new RuntimeException("Unhandled Direction " + direction); - } - } + Gpio getGpio(Pin pin, Direction direction) throws GpioException; /** * Registers the given {@link GpioSignalListener} for changes to {@link Signal}s on the given {@link Gpio} - * + * * @param gpio * the {@link Gpio} being observed * @param listener * the {@link GpioSignalListener} to be notified on changes on the {@link Gpio}'s {@link Signal} - * + * * @throws GpioException * if the {@link Direction} of the {@link Gpio} is not {@link Direction#IN} */ - public void register(Gpio gpio, GpioSignalListener listener) throws GpioException { - - if (gpio.getDirection() != Direction.IN) - throw new GpioException("For reading the direction must be " + Direction.IN); - - synchronized (this.listeners) { - List listeners = this.listeners.get(gpio); - if (listeners == null) { - listeners = new ArrayList<>(); - this.listeners.put(gpio, listeners); - } - - listeners.add(listener); - } - - synchronized (this) { - notifyAll(); - } - } + void register(Gpio gpio, GpioSignalListener listener) throws GpioException; /** * Unregisters a {@link GpioSignalListener} from changes to the given {@link Gpio} - * + * * @param gpio * the {@link Gpio} for which the listener is to be removed * @param listener * the {@link GpioSignalListener} to be removed from changes to the given {@link Gpio} - * + * * @return true if the listener was unregistered, false if not */ - public boolean unregister(GpioBridgeTest gpio, GpioSignalListener listener) { - synchronized (this.listeners) { - List listeners = this.listeners.get(gpio); - if (listeners == null) { - return false; - } - - boolean removed = listeners.remove(listener); - - if (listeners.isEmpty()) - this.listeners.remove(gpio); - - return removed; - } - } -} \ No newline at end of file + boolean unregister(GpioBridgeTest gpio, GpioSignalListener listener); +} diff --git a/src/main/java/ch/eitchnet/beaglebone/GpioBridgeImpl.java b/src/main/java/ch/eitchnet/beaglebone/GpioBridgeImpl.java new file mode 100644 index 0000000..3ec6926 --- /dev/null +++ b/src/main/java/ch/eitchnet/beaglebone/GpioBridgeImpl.java @@ -0,0 +1,398 @@ +package ch.eitchnet.beaglebone; + +import java.io.*; +import java.util.*; + +/** + *

+ * Main object to give access to GPIO ports on a Linux kernel + *

+ * + *

+ * The {@link GpioBridge} is a singleton. Features include retrieving Pins, writing and reading values, as well as + * registering observers for changes to input pins + *

+ * + *

+ * {@link Gpio} objects are cached and their {@link Signal} is set by the {@link GpioBridge} accordingly + *

+ * + * @author Robert von Burg <eitch@eitchnet.ch> + */ +public class GpioBridgeImpl implements GpioBridge { + + private static final String GPIO_PATH = "/sys/class/gpio/"; + + private Map cache; + private Map> listeners; + private Thread thread; + private volatile boolean run; + + private static final GpioBridge instance; + + static { + instance = new GpioBridgeImpl(); + } + + /** + * @return the instance of the {@link GpioBridge} + */ + public static GpioBridge getInstance() { + return instance; + } + + /** + * Private constructor for singleton construction + */ + private GpioBridgeImpl() { + this.cache = new HashMap<>(); + this.listeners = Collections.synchronizedMap(new HashMap<>()); + } + + /** + * Returns the kernel file path to the value of the {@link Gpio} + * + * @param gpio + * the {@link Gpio} for which the path is to be returned + * + * @return the Path to the {@link Gpio}'s value + */ + private File getGpioValuePath(Gpio gpio) { + return new File(GPIO_PATH, gpio.getKernelName() + "/value"); + } + + /** + * Returns the kernel file path to the direction of the {@link Gpio} + * + * @param gpio + * the {@link Gpio} for which the path is to be returned + * + * @return the Path to the {@link Gpio}'s direction + */ + private File getGpioDirectionPath(Gpio gpio) { + return new File(GPIO_PATH, gpio.getKernelName() + "/direction"); + } + + /** + *

+ * Public API method to write the given {@link Signal} on the given {@link Gpio}'s pin. + *

+ * + * @param gpio + * the {@link Gpio} to which the {@link Signal} should be written + * @param signal + * the {@link Signal} to write to the given {@link Gpio} + * + * @throws GpioException + * if the direction of the {@link Gpio} is not {@link Direction#OUT}, or if something goes wrong while + * writing to the file + */ + @Override + public void writeValue(Gpio gpio, Signal signal) throws GpioException { + + synchronized (gpio) { + if (gpio.getDirection() != Direction.OUT) + throw new GpioException("For writing the direction must be " + Direction.OUT); + + File file = getGpioValuePath(gpio); + try (FileOutputStream out = new FileOutputStream(file)) { + + out.write(signal.getValueS().getBytes()); + out.flush(); + + gpio.setSignal(signal); + + } catch (Exception e) { + throw new GpioException("Failed to write GPIO " + gpio + " with signal " + signal, e); + } + + System.out.println("Set GPIO " + gpio.getPin() + " signal to " + gpio.getSignal()); + } + } + + /** + *

+ * Public API method to read the current {@link Signal} on the given {@link Gpio}'s pin. + *

+ * + * @param gpio + * the {@link Gpio} for which the {@link Signal} should be read + * + * @return The {@link Gpio}'s current signal + * + * @throws GpioException + * if the direction of the {@link Gpio} is not {@link Direction#IN}, or if something goes wrong while + * reading from the file + */ + @Override + public Signal readValue(Gpio gpio) throws GpioException { + + synchronized (gpio) { + + if (gpio.getDirection() != Direction.IN) + throw new GpioException("For reading the direction must be " + Direction.IN); + + File file = getGpioValuePath(gpio); + try (BufferedReader fin = new BufferedReader(new FileReader(file))) { + + String valueS = fin.readLine(); + Signal signal = Signal.getSignal(valueS); + if (!gpio.getSignal().equals(signal)) + gpio.setSignal(signal); + + return signal; + + } catch (Exception e) { + throw new GpioException("Failed to read GPIO " + gpio, e); + } + } + } + + /** + * Starts the {@link GpioBridge}'s signal observing {@link Thread}. If no observers are registered with the + * {@link #register(Gpio, GpioSignalListener)}-method, then this method needs not to be called. + */ + @Override + public void start() { + + this.run = true; + this.thread = new Thread(() -> { + while (this.run) { + if (this.listeners.isEmpty()) { + synchronized (this) { + try { + wait(1000l); + } catch (InterruptedException e) { + System.out.println("Was interrupted. Stopping thread."); + this.run = false; + break; + } + } + } else { + + List changes = new ArrayList<>(); + + synchronized (this.listeners) { + for (Gpio gpio : this.listeners.keySet()) { + try { + synchronized (gpio) { + Signal currentSignal = gpio.getSignal(); + Signal newSignal = readValue(gpio); + if (currentSignal != newSignal) + changes.add(gpio); + } + } catch (Exception e) { + System.out.println("Failed to read GPIO " + gpio + " due to:"); + e.printStackTrace(); + this.run = false; + break; + } + } + } + + if (!changes.isEmpty()) + System.out.println("Found " + changes.size() + " GPIO changes."); + + for (Gpio gpio : changes) { + synchronized (this.listeners) { + List listeners = this.listeners.get(gpio); + System.out.println("GPIO " + gpio.getPin() + " changed to " + gpio.getSignal() + + ". Notifying " + listeners.size() + " listeners."); + + for (GpioSignalListener listener : listeners) { + try { + listener.notify(gpio); + } catch (Exception e) { + System.out.println("Failed to update listener " + listener + " due to:"); + e.printStackTrace(); + } + } + } + } + + try { + Thread.sleep(200l); + } catch (InterruptedException e) { + System.out.println("Was interrupted. Stopping thread."); + this.run = false; + break; + } + } + } + }, "gpio_reader"); + this.thread.start(); + System.out.println("Started GPIO bridge."); + } + + /** + * Stops observing any pins and stops the {@link Thread} + */ + @Override + public void stop() { + this.run = false; + this.thread.interrupt(); + try { + this.thread.join(5000l); + } catch (InterruptedException e) { + System.out.println("Was interrupted while waiting for thread to stop?!"); + } + } + + /** + *

+ * Returns the {@link Gpio} for the given {@link Direction}. + *

+ * + *

+ * Note: This method can not be called multiple times with different {@link Direction}s. The + * {@link GpioBridge} does not handle pins that are simultaneously input and output as this is not supported by the + * Linux kernel. + *

+ * + * @param pin + * The {@link Pin} for which the {@link Gpio} in the given {@link Direction} is to be returned + * @param direction + * the {@link Direction} for which this {@link Gpio} is to be returned + * + * @return The {@link Gpio} with the configured {@link Direction} + * + * @throws GpioException + * If the given {@link Direction} does not match the kernel's configured direction, or if the file + * permissions are not set so that the Java process can access the file (read access for input pin, + * write access for output pin. + */ + @Override + public synchronized Gpio getGpio(Pin pin, Direction direction) throws GpioException { + Gpio gpio = this.cache.get(pin); + if (gpio == null) { + + gpio = new Gpio(pin, direction); + + // validate direction + assertDirection(gpio); + + // validate file permissions + validateFilePermissions(gpio); + + this.cache.put(pin, gpio); + System.out.println("Initialized pin " + pin + " with direction " + direction + "."); + } + + return gpio; + } + + /** + * Validates the direction of the {@link Gpio} is the same as kernel's exported state + * + * @param gpio + * the {@link Gpio} been asserted for direction + * + * @throws GpioException + * if the assertion fails + */ + private void assertDirection(Gpio gpio) throws GpioException { + File file = getGpioDirectionPath(gpio); + Pin pin = gpio.getPin(); + try (BufferedReader fin = new BufferedReader(new FileReader(file))) { + + String directionS = fin.readLine(); + Direction dir = Direction.getDirection(directionS); + if (dir != gpio.getDirection()) + throw new GpioException("Actual direction of GPIO " + pin + " is " + dir + " not " + directionS); + + } catch (FileNotFoundException e) { + throw new GpioException("GPIO " + pin + " does not exist, was the pin exported to user space?", e); + } catch (IOException e) { + throw new GpioException("Failed to open GPIO " + pin, e); + } + } + + /** + * Validates the file permissions of the {@link Gpio} is correct for the {@link Gpio}'s {@link Direction}: + *
    + *
  • read access for input pin
  • + *
  • write access for output pin
  • + *
+ * + * @param gpio + * the {@link Gpio} been asserted for direction + * + * @throws GpioException + * if the assertion fails + */ + private void validateFilePermissions(Gpio gpio) throws GpioException { + File gpioValuePath = getGpioValuePath(gpio); + Direction direction = gpio.getDirection(); + if (direction == Direction.IN) { + if (!gpioValuePath.canRead()) + throw new GpioException("GPIO " + gpio + " has direction " + direction + + " and is not readable. Are the file permissions ok?"); + + } else if (direction == Direction.OUT) { + if (!gpioValuePath.canWrite()) + throw new GpioException("GPIO " + gpio + " has direction " + direction + + " and is not writable. Are the file permissions ok?"); + } else { + throw new RuntimeException("Unhandled Direction " + direction); + } + } + + /** + * Registers the given {@link GpioSignalListener} for changes to {@link Signal}s on the given {@link Gpio} + * + * @param gpio + * the {@link Gpio} being observed + * @param listener + * the {@link GpioSignalListener} to be notified on changes on the {@link Gpio}'s {@link Signal} + * + * @throws GpioException + * if the {@link Direction} of the {@link Gpio} is not {@link Direction#IN} + */ + @Override + public void register(Gpio gpio, GpioSignalListener listener) throws GpioException { + + if (gpio.getDirection() != Direction.IN) + throw new GpioException("For reading the direction must be " + Direction.IN); + + synchronized (this.listeners) { + List listeners = this.listeners.get(gpio); + if (listeners == null) { + listeners = new ArrayList<>(); + this.listeners.put(gpio, listeners); + } + + listeners.add(listener); + } + + synchronized (this) { + notifyAll(); + } + } + + /** + * Unregisters a {@link GpioSignalListener} from changes to the given {@link Gpio} + * + * @param gpio + * the {@link Gpio} for which the listener is to be removed + * @param listener + * the {@link GpioSignalListener} to be removed from changes to the given {@link Gpio} + * + * @return true if the listener was unregistered, false if not + */ + @Override + public boolean unregister(GpioBridgeTest gpio, GpioSignalListener listener) { + synchronized (this.listeners) { + List listeners = this.listeners.get(gpio); + if (listeners == null) { + return false; + } + + boolean removed = listeners.remove(listener); + + if (listeners.isEmpty()) + this.listeners.remove(gpio); + + return removed; + } + } +} \ No newline at end of file diff --git a/src/main/java/ch/eitchnet/beaglebone/GpioBridgeTest.java b/src/main/java/ch/eitchnet/beaglebone/GpioBridgeTest.java index be755f4..c9b8fb7 100644 --- a/src/main/java/ch/eitchnet/beaglebone/GpioBridgeTest.java +++ b/src/main/java/ch/eitchnet/beaglebone/GpioBridgeTest.java @@ -28,7 +28,7 @@ public class GpioBridgeTest { Thread.currentThread().setPriority(Thread.MAX_PRIORITY); - GpioBridge gpioBridge = GpioBridge.getInstance(); + GpioBridge gpioBridge = GpioBridgeImpl.getInstance(); System.out.println("Preparing pins..."); greenBtn = gpioBridge.getGpio(Pin.P8_07, Direction.IN).setLabel("Green");