/**
* 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.Paths;
import java.util.Dictionary;
import java.util.HashMap;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import org.openhab.io.gpio.GPIO;
import org.openhab.io.gpio.GPIOPin;
import org.osgi.service.cm.ConfigurationException;
import org.osgi.service.cm.ManagedService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Implementation of <code>GPIO</code> interface for boards running
* Linux OS. Based on kernel GPIO framework exposed to user space
* through <code>sysfs</code> pseudo file system.
*
* @author Dancho Penev
* @since 1.5.0
*/
public class GPIOLinux implements GPIO, ManagedService {
private static final Charset DEFAULT_ENCODING = StandardCharsets.UTF_8;
private static final String SYSFS_VFSTYPE = "sysfs";
private static final String MTAB_PATH = "/proc/mounts";
private static final String MTAB_FIELD_SEPARATOR = " ";
private static final long GPIOLOCK_TIMEOUT = 10;
private static final TimeUnit GPIOLOCK_TIMEOUT_UNITS = TimeUnit.SECONDS;
private static final String PROP_DEBOUNCE_INTERVAL = "debounce";
private static final String PROP_FORCE = "force";
private static final Logger logger = LoggerFactory.getLogger(GPIOLinux.class);
/** Path to directory where <code>sysfs</code> is mounted. */
private String sysFS = null;
/** Default debounce interval in milliseconds */
private volatile long defaultDebounceInterval = 0;
/** Forcibly use the pins after unclean shutdown */
private volatile boolean defaultForce = false;
/** GPIO subsystem read/write lock. */
private final ReentrantReadWriteLock gpioLock = new ReentrantReadWriteLock();
/** Database for GPIO pins which are in use. */
private final HashMap<GPIOPin, Integer> gpioRegistry = new HashMap<GPIOPin, Integer>();
/**
* Discovers existing mount point for <code>sysfs</code> pseudo file system.
*/
public GPIOLinux() {
try {
sysFS = getMountPoint(SYSFS_VFSTYPE);
} catch (IOException e) {
logger.error("Automatic mount point discovering for pseudo file system '" + SYSFS_VFSTYPE + "' failed. "
+ "If 'procfs' isn't mounted and mount point is set in configuration file this error can be omitted. "
+ "Error: " + e.getMessage());
}
}
/**
* Called when <code>Configuration Admin</code> detects configuration
* change. Sets manually configured mount points for <code>sysfs</code>
* pseudo file systems, overwrites this what was discovered. Also set
* default debounce interval if exist.
*/
public void updated(Dictionary<String, ?> properties) throws ConfigurationException {
if (properties != null) {
String propSysFS = Objects.toString(properties.get(SYSFS_VFSTYPE), null);
if (propSysFS != null) {
try {
if (isFSMounted(SYSFS_VFSTYPE, propSysFS)) {
sysFS = propSysFS;
} else {
logger.error("Configured mount point is invalid, '" + SYSFS_VFSTYPE + "' isn't mounted at '"
+ propSysFS + "'");
throw new ConfigurationException(SYSFS_VFSTYPE, "Configured mount point is invalid, '"
+ SYSFS_VFSTYPE + "' isn't mounted at '" + propSysFS + "'");
}
} catch (IOException e) {
logger.error(
"Checking whether pseudo file system '" + SYSFS_VFSTYPE + "' is mounted or not failed. "
+ "If 'procfs' isn't mounted this error can be omitted. Error: " + e.getMessage());
sysFS = propSysFS;
}
}
String propDebounceInterval = Objects.toString(properties.get(PROP_DEBOUNCE_INTERVAL), null);
if (propDebounceInterval != null) {
try {
long debounceInterval = Long.parseLong(propDebounceInterval);
if (debounceInterval >= 0) {
defaultDebounceInterval = debounceInterval;
} else {
logger.error(
"Configured " + PROP_DEBOUNCE_INTERVAL + " is invalid, must not be negative value");
throw new ConfigurationException(PROP_DEBOUNCE_INTERVAL,
"Configured " + PROP_DEBOUNCE_INTERVAL + " is invalid, must not be negative value");
}
} catch (NumberFormatException e) {
logger.error("Configured " + PROP_DEBOUNCE_INTERVAL + " is invalid, must be numeric value");
throw new ConfigurationException(PROP_DEBOUNCE_INTERVAL,
"Configured " + PROP_DEBOUNCE_INTERVAL + " is invalid, must be numeric value");
}
}
String propForce = Objects.toString(properties.get(PROP_FORCE), null);
if (propForce != null) {
defaultForce = Boolean.parseBoolean(propForce);
}
}
}
/**
* Determines mount point for given file system type.
*
* @param vfsType the type of file system to search for
* @return the first found mount point if the file system is mounted,
* <code>null</code> otherwise
* @throws IOException if reading of "mtab" file fails
*/
private String getMountPoint(String vfsType) throws IOException {
List<String> mtabLines = Files.readAllLines(Paths.get(MTAB_PATH), DEFAULT_ENCODING);
for (String mtabRecord : mtabLines) {
String[] mtabRecordFields = mtabRecord.split(MTAB_FIELD_SEPARATOR);
if (mtabRecordFields[2].compareToIgnoreCase(vfsType) == 0) {
return mtabRecordFields[1];
}
}
return null;
}
/**
* Checks whether file system from given type is mounted or not
* at specific location.
*
* @param vfsType the type of file system to check
* @param mountPoint directory at which file system should be
* mounted
* @return <code>true</code> if the file system of provided type is
* mounted at provided location, <code>false</code> otherwise
* @throws IOException if reading of "mtab" file fails
*/
private boolean isFSMounted(String vfsType, String mountPoint) throws IOException {
List<String> mtabLines = Files.readAllLines(Paths.get(MTAB_PATH), DEFAULT_ENCODING);
for (String mtabRecord : mtabLines) {
String[] mtabRecordFields = mtabRecord.split(MTAB_FIELD_SEPARATOR);
if ((mtabRecordFields[2].compareToIgnoreCase(vfsType) == 0)
&& (mtabRecordFields[1].compareToIgnoreCase(mountPoint) == 0)) {
return true;
}
}
return false;
}
public GPIOPin reservePin(Integer pinNumber) throws IOException {
return reservePin(pinNumber, false);
}
/**
* Exports the pin to user space, creates and initializes the
* backend object representing the pin. Updates the registry for
* initialized pins.
*/
public GPIOPin reservePin(Integer pinNumber, boolean force) throws IOException {
final String SYSFS_CLASS_GPIO = sysFS + "/class/gpio/";
GPIOPinLinux pin = null;
/*
* Variable 'sysFS' may be null if mandatory pseudo file system 'sysfs' isn't mounted or mount point can't be
* determined.
*/
if (sysFS == null) {
throw new IOException("Mount point for '" + SYSFS_VFSTYPE + "' isn't configured and can't be determined");
}
/* Sanity check, negative pin number is illegal. */
if (pinNumber < 0) {
throw new IllegalArgumentException("Unsupported argument for 'pinNumber' parameter (" + pinNumber + ")");
}
/* Acquiring write lock guarantees atomic check/set operation */
try {
if (gpioLock.writeLock().tryLock(GPIOLOCK_TIMEOUT, GPIOLOCK_TIMEOUT_UNITS)) {
try {
if (gpioRegistry.containsValue(pinNumber)) {
throw new IllegalArgumentException(
"The pin with number '" + pinNumber + "' is already registered");
}
/* Exports the pin to user space. */
try {
Files.write(Paths.get(SYSFS_CLASS_GPIO + "export"), pinNumber.toString().getBytes());
} catch (IOException e) {
if (force || defaultForce) {
/* Forcibly use the pin as unexport it and export it again */
Files.write(Paths.get(SYSFS_CLASS_GPIO + "unexport"), pinNumber.toString().getBytes());
Files.write(Paths.get(SYSFS_CLASS_GPIO + "export"), pinNumber.toString().getBytes());
logger.warn(
"The control on GPIO pin with number '" + pinNumber + "' was forcibly acquired");
} else {
throw e;
}
}
/* Wait for the export to proceed */
Thread.sleep(500);
/* Create backend object */
pin = new GPIOPinLinux(pinNumber, SYSFS_CLASS_GPIO + "gpio" + pinNumber, defaultDebounceInterval);
/* Register the pin */
gpioRegistry.put(pin, pinNumber);
} finally {
gpioLock.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 lock can't be aquired for " + GPIOLOCK_TIMEOUT + " "
+ GPIOLOCK_TIMEOUT_UNITS.toString());
}
} catch (InterruptedException e) {
throw new IOException("The thread was interrupted while waiting for write GPIO lock");
}
return pin;
}
/**
* Removes the pin from internal registry, uninitialize the object
* and stop pin export to user space.
*/
public void releasePin(GPIOPin pin) throws IOException {
try {
if (gpioLock.writeLock().tryLock(GPIOLOCK_TIMEOUT, GPIOLOCK_TIMEOUT_UNITS)) {
try {
final String SYSFS_CLASS_GPIO = sysFS + "/class/gpio/";
/* Unregister the pin */
Integer pinNumber = gpioRegistry.remove(pin);
if (pinNumber == null) {
throw new IllegalArgumentException("The pin object isn't registered");
}
((GPIOPinLinux) pin).stopEventProcessing();
/* May throw "IllegalArgumentException" if the pin isn't exported */
Files.write(Paths.get(SYSFS_CLASS_GPIO + "unexport"), pinNumber.toString().getBytes());
} finally {
gpioLock.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 lock can't be aquired for " + GPIOLOCK_TIMEOUT + " "
+ GPIOLOCK_TIMEOUT_UNITS.toString());
}
} catch (InterruptedException e) {
throw new IOException("The thread was interrupted while waiting for write GPIO lock");
}
}
public long getDefaultDebounceInterval() {
return defaultDebounceInterval;
}
}