Skip to content

Commit

Permalink
Refactoring and JavaDoc
Browse files Browse the repository at this point in the history
  • Loading branch information
DigitalSmile committed Jan 9, 2024
1 parent 6724899 commit 3770f0d
Show file tree
Hide file tree
Showing 24 changed files with 634 additions and 58 deletions.
53 changes: 52 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,53 @@
[![](https://jitpack.io/v/DigitalSmile/gpio.svg)](https://jitpack.io/#DigitalSmile/gpio)
# GPIO library with new java FFM API
![](https://img.shields.io/badge/Java-21+-success)
![GitHub Actions Workflow Status](https://img.shields.io/github/actions/workflow/status/digitalsmile/gpio/gradle.yml)
---
# Java GPIO library with new java FFM API
With new FFM API that were released in a recent versions of Java you can work with any kind of hardware just within your app. No more JNI and JNA!

Read more about FFM in here -> https://openjdk.org/jeps/442

Since Java 22 it will be the default option for usage the external C libraries. Be prepared now!

## Features
1) Zero dependencies (except SLF4J for logging)
2) Modern Java 21+ with full language feature support
3) Supports working with individual GPIO Pins
4) Supports working with SPI
5) Tested on Raspberry Pi 4.
6) i2c and UART/TTL coming soon.

## Usage
1) Add a dependency.
```groovy
allprojects {
repositories {
...
maven { url 'https://jitpack.io' }
}
}
dependencies {
implementation 'com.github.DigitalSmile:gpio:{version}'
}
```
2) <b>IMPORTANT:</b> add Java VM Option `--enable-preview` in your IDE and gradle -> https://stackoverflow.com/questions/72083752/enable-preview-features-in-an-early-access-version-of-java-in-intellij (that will go away when JAva will be upgraded to 22)
3) Add to your code:

```java
var spiBus = GPIOBoard.ofSPI(0, SPIMode.MODE_0, 20_000_000);

var rst = GPIOBoard.ofPin(17, Direction.OUTPUT);
var busy = GPIOBoard.ofPin(24, Direction.INPUT);
var dc = GPIOBoard.ofPin(25, Direction.OUTPUT);
var pwr = GPIOBoard.ofPin(18, Direction.OUTPUT);

pwr.write(State.HIGH);
while (busy.read().equals(State.HIGH)) {
Thread.sleep(10);
}

dc.write(State.LOW);
spiBus.sendByteData(new byte[]{1}, false);
```
4) Enjoy! :)
4 changes: 4 additions & 0 deletions src/main/java/module-info.java
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
/**
* Main GPIO module
*/
module gpio.main {
requires org.slf4j;

exports org.digitalsmile.gpio.core;
exports org.digitalsmile.gpio.pin;
exports org.digitalsmile.gpio.pin.attributes;
exports org.digitalsmile.gpio.spi;
exports org.digitalsmile.gpio.pin.event;
exports org.digitalsmile.gpio;
}
73 changes: 72 additions & 1 deletion src/main/java/org/digitalsmile/gpio/GPIOBoard.java
Original file line number Diff line number Diff line change
Expand Up @@ -11,45 +11,116 @@

import java.io.IOException;

public class GPIOBoard {
/**
* Class for creating abstractions over GPIO. It uses native FFM calls (such as open and ioctl) to operate with hardware.
* Please, consider creating all interfaces through this general class.
*/
public final class GPIOBoard {

private static final String DEFAULT_GPIO_DEVICE = "/dev/gpiochip0";
private static final String BASE_SPI_PATH = "/dev/spidev0.";

private static InfoStruct infoStruct;

/**
* Forbids creating an instance of this class.
*/
private GPIOBoard() {
}

/**
* Initializes the GPIO Board by given device name.
*
* @param deviceName - given device name
*/
private static void initialize(String deviceName) {
if (infoStruct == null) {
var fd = FileDescriptor.open(deviceName);
infoStruct = IOCtl.call(fd, Command.getGpioGetChipInfoIoctl(), new InfoStruct(new byte[]{}, new byte[]{}, 0));
}
}

/**
* Creates GPIO Pin using GPIO device name, pin number and direction.
*
* @param gpioDeviceName - GPIO device name
* @param pinNumber - pin number
* @param direction - direction
* @return GPIO Pin instance
* @throws IOException if errors occurred during creating instance
*/
public static Pin ofPin(String gpioDeviceName, int pinNumber, Direction direction) throws IOException {
initialize(gpioDeviceName);
return new Pin(gpioDeviceName, pinNumber, direction);
}

/**
* Creates GPIO Pin using just pin number. All other fields are defaults.
*
* @param pinNumber - pin number
* @return GPIO Pin instance
* @throws IOException if errors occurred during creating instance
*/
public static Pin ofPin(int pinNumber) throws IOException {
return ofPin(DEFAULT_GPIO_DEVICE, pinNumber, Direction.OUTPUT);
}

/**
* Creates GPIO Pin using just pin number and direction. All other fields are defaults.
*
* @param pinNumber - pin number
* @param direction - direction
* @return GPIO Pin instance
* @throws IOException if errors occurred during creating instance
*/
public static Pin ofPin(int pinNumber, Direction direction) throws IOException {
return ofPin(DEFAULT_GPIO_DEVICE, pinNumber, direction);
}

/**
* Creates SPI Bus from given GPIO device name, path to spi bus, bus number, spi mode, clock frequency, length of byte and bit order.
*
* @param gpioDeviceName - GPIO device name
* @param spiPath - path to spi bus
* @param busNumber - bus number
* @param spiMode - spi mode
* @param clockFrequency - clock frequency
* @param byteLength - length if byte
* @param bitOrdering - bit order
* @return SPI Bus instance
* @throws IOException if errors occurred during creating instance
*/
public static SPIBus ofSPI(String gpioDeviceName, String spiPath, int busNumber, SPIMode spiMode, int clockFrequency, int byteLength,
int bitOrdering) throws IOException {
initialize(gpioDeviceName);
return new SPIBus(spiPath, busNumber, spiMode, clockFrequency, byteLength, bitOrdering);
}

/**
* Creates SPI Bus from given bus number, spi mode, clock frequency, length of byte and bit order. All other fields are defaults.
*
* @param busNumber - bus number
* @param spiMode - spi mode
* @param clockFrequency - clock frequency
* @param byteLength - length if byte
* @param bitOrdering - bit order
* @return SPI Bus instance
* @throws IOException if errors occurred during creating instance
*/
public static SPIBus ofSPI(int busNumber, SPIMode spiMode, int clockFrequency, int byteLength,
int bitOrdering) throws IOException {
return ofSPI(DEFAULT_GPIO_DEVICE, BASE_SPI_PATH, busNumber, spiMode, clockFrequency, byteLength, bitOrdering);
}

/**
* Creates SPI Bus from given bus number, spi mode, clock frequency. All other fields are defaults.
*
* @param busNumber - bus number
* @param spiMode - spi mode
* @param clockFrequency - clock frequency
* @return SPI Bus instance
* @throws IOException if errors occurred during creating instance
*/
public static SPIBus ofSPI(int busNumber, SPIMode spiMode, int clockFrequency) throws IOException {
return ofSPI(DEFAULT_GPIO_DEVICE, BASE_SPI_PATH, busNumber, spiMode, clockFrequency, 8, 0);
}
Expand Down
15 changes: 15 additions & 0 deletions src/main/java/org/digitalsmile/gpio/core/IntegerToHex.java
Original file line number Diff line number Diff line change
@@ -1,8 +1,23 @@
package org.digitalsmile.gpio.core;

/**
* Helper class to convert from integer (byte) to hex string.
*/
public class IntegerToHex {
private static final String digits = "0123456789abcdef";

/**
* Forbids creating an instance of this class.
*/
private IntegerToHex() {
}

/**
* Converts given integer (byte) input into hexadecimal string.
*
* @param input - integer (byte) input to convert
* @return hexadecimal string representation of integer (byte)
*/
public static String convert(int input) {
if (input <= 0) {
return "0x0";
Expand Down
22 changes: 22 additions & 0 deletions src/main/java/org/digitalsmile/gpio/core/NativeMemoryLayout.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,34 @@
import java.lang.foreign.MemoryLayout;
import java.lang.foreign.MemorySegment;

/**
* Interface that provides memory layout of class file for using with FFM API of recent Java versions.
*/
public interface NativeMemoryLayout {

/**
* Gets {@link MemoryLayout} from a class / object (structure).
*
* @return memory layout of class / object
*/
MemoryLayout getMemoryLayout();

/**
* Converts {@link MemorySegment} buffer to a class / object structure.
*
* @param buffer - memory segment to convert from
* @param <T> type of converted class / object structure
* @return new class / object structure from a given buffer
* @throws Throwable unchecked exception
*/
<T> T fromBytes(MemorySegment buffer) throws Throwable;

/**
* Converts a class / object structure into a {@link MemorySegment} buffer.
*
* @param buffer - buffer to be filled with class / object structure
* @throws Throwable unchecked exception
*/
void toBytes(MemorySegment buffer) throws Throwable;

}
70 changes: 56 additions & 14 deletions src/main/java/org/digitalsmile/gpio/core/file/FileDescriptor.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@
import java.lang.invoke.MethodHandle;
import java.util.Arrays;

/**
* Class for calling simple operations (open, close, write, read) through native Java interface (FFM), introduced in recent versions of Java.
* All methods are static and stateless. They are using standard kernel library (libc) calls to interact with native code.
* Since this class is internal, the log level is set to trace.
*/
public final class FileDescriptor {
private static final Logger logger = LoggerFactory.getLogger(FileDescriptor.class);

Expand All @@ -24,44 +29,76 @@ public final class FileDescriptor {
STD_LIB.find("write").orElseThrow(),
FunctionDescriptor.of(ValueLayout.JAVA_INT, ValueLayout.JAVA_INT, ValueLayout.ADDRESS, ValueLayout.JAVA_INT));

/**
* Forbids creating an instance of this class.
*/
private FileDescriptor() {
}

/**
* Opens file at path with selected flag ({@link Flag}).
*
* @param path - the file to open
* @param openFlag - flag to handle with file ({@link Flag})
* @return file descriptor if file is successfully open
*/
public static int open(String path, int openFlag) {
logger.trace("Opening {}", path);
var fd = 0;
try (Arena offHeap = Arena.ofConfined()) {
var str = offHeap.allocateUtf8String(path);
fd = (int) OPEN64.invoke(str, openFlag);
if (fd < 0) {
throw new RuntimeException("File " + path + " is not readable!");
}
} catch (Throwable e) {
throw new RuntimeException(e);
}
if (fd == -1) {
throw new RuntimeException("File " + path + " is not readable!");
}
logger.trace("Opened {} with file descriptor {}", path, fd);
return fd;
}

public static int close(int fd) {
/**
* Opens file at path with flag {@link Flag}
*
* @param path - the file to open
* @return file descriptor if file is successfully open
*/
public static int open(String path) {
return open(path, Flag.O_RDWR);
}


/**
* Closes given file descriptor.
*
* @param fd - file descriptor to close
*/
public static void close(int fd) {
logger.trace("Closing file descriptor {}", fd);
try {
var result = (int) CLOSE.invoke(fd);
if (result < 0) {
throw new RuntimeException("Cannot close file with descriptor " + fd);
}
logger.trace("Closed file descriptor with result {}", result);
return result;
} catch (Throwable e) {
throw new RuntimeException(e);
}
}



public static int open(String path) {
return open(path, Flags.O_RDWR);
}

/**
* Reads file descriptor with predefined size.
*
* @param fd - file descriptor to read
* @param size - size of the byte buffer to read into
* @return byte array with contents of the read file descriptor
*/
public static byte[] read(int fd, int size) {
logger.trace("Reading file descriptor {}", fd);
var byteResult = new byte[]{};
var byteResult = new byte[size];
try (Arena offHeap = Arena.ofConfined()) {
var bufferMemorySegment = offHeap.allocateArray(ValueLayout.JAVA_BYTE, new byte[size]);
var bufferMemorySegment = offHeap.allocateArray(ValueLayout.JAVA_BYTE, byteResult);
var read = (int) READ.invoke(fd, bufferMemorySegment, size);
if (read != size) {
throw new RuntimeException("Read " + read + " bytes, but size was " + size);
Expand All @@ -75,6 +112,12 @@ public static byte[] read(int fd, int size) {
return byteResult;
}

/**
* Writes byte array data to the provided file descriptor.
*
* @param fd - file descriptor to write
* @param data - byte array of data to write
*/
public static void write(int fd, byte[] data) {
logger.trace("Writing to file descriptor {} with data {}", fd, Arrays.toString(data));
try (Arena offHeap = Arena.ofConfined()) {
Expand All @@ -89,5 +132,4 @@ public static void write(int fd, byte[] data) {
}
logger.trace("Wrote to file descriptor {}", fd);
}

}
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package org.digitalsmile.gpio.core.file;

public final class Flags {
/**
* Kernel flags for open command.
*/
public final class Flag {

public static final int O_APPEND = 1024;
public static final int O_ASYNC = 8192;
Expand Down
9 changes: 9 additions & 0 deletions src/main/java/org/digitalsmile/gpio/core/ioctl/Command.java
Original file line number Diff line number Diff line change
@@ -1,7 +1,16 @@
package org.digitalsmile.gpio.core.ioctl;

/**
* Commands to be provided for ioctl calls.
*/
public final class Command {

/**
* Forbids creating an instance of this class.
*/
private Command() {
}

public static long getGpioGetChipInfoIoctl() {
return Internals.GPIO_GET_CHIPINFO_IOCTL;
}
Expand Down
Loading

0 comments on commit 3770f0d

Please sign in to comment.