Skip to content
This repository was archived by the owner on Feb 20, 2026. It is now read-only.

Commit 3770f0d

Browse files
committed
Refactoring and JavaDoc
1 parent 6724899 commit 3770f0d

24 files changed

+634
-58
lines changed

README.md

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,53 @@
11
[![](https://jitpack.io/v/DigitalSmile/gpio.svg)](https://jitpack.io/#DigitalSmile/gpio)
2-
# GPIO library with new java FFM API
2+
![](https://img.shields.io/badge/Java-21+-success)
3+
![GitHub Actions Workflow Status](https://img.shields.io/github/actions/workflow/status/digitalsmile/gpio/gradle.yml)
4+
---
5+
# Java GPIO library with new java FFM API
6+
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!
7+
8+
Read more about FFM in here -> https://openjdk.org/jeps/442
9+
10+
Since Java 22 it will be the default option for usage the external C libraries. Be prepared now!
11+
12+
## Features
13+
1) Zero dependencies (except SLF4J for logging)
14+
2) Modern Java 21+ with full language feature support
15+
3) Supports working with individual GPIO Pins
16+
4) Supports working with SPI
17+
5) Tested on Raspberry Pi 4.
18+
6) i2c and UART/TTL coming soon.
19+
20+
## Usage
21+
1) Add a dependency.
22+
```groovy
23+
allprojects {
24+
repositories {
25+
...
26+
maven { url 'https://jitpack.io' }
27+
}
28+
}
29+
30+
dependencies {
31+
implementation 'com.github.DigitalSmile:gpio:{version}'
32+
}
33+
```
34+
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)
35+
3) Add to your code:
36+
37+
```java
38+
var spiBus = GPIOBoard.ofSPI(0, SPIMode.MODE_0, 20_000_000);
39+
40+
var rst = GPIOBoard.ofPin(17, Direction.OUTPUT);
41+
var busy = GPIOBoard.ofPin(24, Direction.INPUT);
42+
var dc = GPIOBoard.ofPin(25, Direction.OUTPUT);
43+
var pwr = GPIOBoard.ofPin(18, Direction.OUTPUT);
44+
45+
pwr.write(State.HIGH);
46+
while (busy.read().equals(State.HIGH)) {
47+
Thread.sleep(10);
48+
}
49+
50+
dc.write(State.LOW);
51+
spiBus.sendByteData(new byte[]{1}, false);
52+
```
53+
4) Enjoy! :)

src/main/java/module-info.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
1+
/**
2+
* Main GPIO module
3+
*/
14
module gpio.main {
25
requires org.slf4j;
36

47
exports org.digitalsmile.gpio.core;
58
exports org.digitalsmile.gpio.pin;
69
exports org.digitalsmile.gpio.pin.attributes;
710
exports org.digitalsmile.gpio.spi;
11+
exports org.digitalsmile.gpio.pin.event;
812
exports org.digitalsmile.gpio;
913
}

src/main/java/org/digitalsmile/gpio/GPIOBoard.java

Lines changed: 72 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,45 +11,116 @@
1111

1212
import java.io.IOException;
1313

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

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

1923
private static InfoStruct infoStruct;
2024

25+
/**
26+
* Forbids creating an instance of this class.
27+
*/
28+
private GPIOBoard() {
29+
}
2130

31+
/**
32+
* Initializes the GPIO Board by given device name.
33+
*
34+
* @param deviceName - given device name
35+
*/
2236
private static void initialize(String deviceName) {
2337
if (infoStruct == null) {
2438
var fd = FileDescriptor.open(deviceName);
2539
infoStruct = IOCtl.call(fd, Command.getGpioGetChipInfoIoctl(), new InfoStruct(new byte[]{}, new byte[]{}, 0));
2640
}
2741
}
2842

43+
/**
44+
* Creates GPIO Pin using GPIO device name, pin number and direction.
45+
*
46+
* @param gpioDeviceName - GPIO device name
47+
* @param pinNumber - pin number
48+
* @param direction - direction
49+
* @return GPIO Pin instance
50+
* @throws IOException if errors occurred during creating instance
51+
*/
2952
public static Pin ofPin(String gpioDeviceName, int pinNumber, Direction direction) throws IOException {
3053
initialize(gpioDeviceName);
3154
return new Pin(gpioDeviceName, pinNumber, direction);
3255
}
3356

57+
/**
58+
* Creates GPIO Pin using just pin number. All other fields are defaults.
59+
*
60+
* @param pinNumber - pin number
61+
* @return GPIO Pin instance
62+
* @throws IOException if errors occurred during creating instance
63+
*/
3464
public static Pin ofPin(int pinNumber) throws IOException {
3565
return ofPin(DEFAULT_GPIO_DEVICE, pinNumber, Direction.OUTPUT);
3666
}
3767

68+
/**
69+
* Creates GPIO Pin using just pin number and direction. All other fields are defaults.
70+
*
71+
* @param pinNumber - pin number
72+
* @param direction - direction
73+
* @return GPIO Pin instance
74+
* @throws IOException if errors occurred during creating instance
75+
*/
3876
public static Pin ofPin(int pinNumber, Direction direction) throws IOException {
3977
return ofPin(DEFAULT_GPIO_DEVICE, pinNumber, direction);
4078
}
4179

80+
/**
81+
* Creates SPI Bus from given GPIO device name, path to spi bus, bus number, spi mode, clock frequency, length of byte and bit order.
82+
*
83+
* @param gpioDeviceName - GPIO device name
84+
* @param spiPath - path to spi bus
85+
* @param busNumber - bus number
86+
* @param spiMode - spi mode
87+
* @param clockFrequency - clock frequency
88+
* @param byteLength - length if byte
89+
* @param bitOrdering - bit order
90+
* @return SPI Bus instance
91+
* @throws IOException if errors occurred during creating instance
92+
*/
4293
public static SPIBus ofSPI(String gpioDeviceName, String spiPath, int busNumber, SPIMode spiMode, int clockFrequency, int byteLength,
4394
int bitOrdering) throws IOException {
4495
initialize(gpioDeviceName);
4596
return new SPIBus(spiPath, busNumber, spiMode, clockFrequency, byteLength, bitOrdering);
4697
}
4798

99+
/**
100+
* Creates SPI Bus from given bus number, spi mode, clock frequency, length of byte and bit order. All other fields are defaults.
101+
*
102+
* @param busNumber - bus number
103+
* @param spiMode - spi mode
104+
* @param clockFrequency - clock frequency
105+
* @param byteLength - length if byte
106+
* @param bitOrdering - bit order
107+
* @return SPI Bus instance
108+
* @throws IOException if errors occurred during creating instance
109+
*/
48110
public static SPIBus ofSPI(int busNumber, SPIMode spiMode, int clockFrequency, int byteLength,
49111
int bitOrdering) throws IOException {
50112
return ofSPI(DEFAULT_GPIO_DEVICE, BASE_SPI_PATH, busNumber, spiMode, clockFrequency, byteLength, bitOrdering);
51113
}
52114

115+
/**
116+
* Creates SPI Bus from given bus number, spi mode, clock frequency. All other fields are defaults.
117+
*
118+
* @param busNumber - bus number
119+
* @param spiMode - spi mode
120+
* @param clockFrequency - clock frequency
121+
* @return SPI Bus instance
122+
* @throws IOException if errors occurred during creating instance
123+
*/
53124
public static SPIBus ofSPI(int busNumber, SPIMode spiMode, int clockFrequency) throws IOException {
54125
return ofSPI(DEFAULT_GPIO_DEVICE, BASE_SPI_PATH, busNumber, spiMode, clockFrequency, 8, 0);
55126
}

src/main/java/org/digitalsmile/gpio/core/IntegerToHex.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,23 @@
11
package org.digitalsmile.gpio.core;
22

3+
/**
4+
* Helper class to convert from integer (byte) to hex string.
5+
*/
36
public class IntegerToHex {
47
private static final String digits = "0123456789abcdef";
58

9+
/**
10+
* Forbids creating an instance of this class.
11+
*/
12+
private IntegerToHex() {
13+
}
14+
15+
/**
16+
* Converts given integer (byte) input into hexadecimal string.
17+
*
18+
* @param input - integer (byte) input to convert
19+
* @return hexadecimal string representation of integer (byte)
20+
*/
621
public static String convert(int input) {
722
if (input <= 0) {
823
return "0x0";

src/main/java/org/digitalsmile/gpio/core/NativeMemoryLayout.java

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,34 @@
33
import java.lang.foreign.MemoryLayout;
44
import java.lang.foreign.MemorySegment;
55

6+
/**
7+
* Interface that provides memory layout of class file for using with FFM API of recent Java versions.
8+
*/
69
public interface NativeMemoryLayout {
710

11+
/**
12+
* Gets {@link MemoryLayout} from a class / object (structure).
13+
*
14+
* @return memory layout of class / object
15+
*/
816
MemoryLayout getMemoryLayout();
917

18+
/**
19+
* Converts {@link MemorySegment} buffer to a class / object structure.
20+
*
21+
* @param buffer - memory segment to convert from
22+
* @param <T> type of converted class / object structure
23+
* @return new class / object structure from a given buffer
24+
* @throws Throwable unchecked exception
25+
*/
1026
<T> T fromBytes(MemorySegment buffer) throws Throwable;
1127

28+
/**
29+
* Converts a class / object structure into a {@link MemorySegment} buffer.
30+
*
31+
* @param buffer - buffer to be filled with class / object structure
32+
* @throws Throwable unchecked exception
33+
*/
1234
void toBytes(MemorySegment buffer) throws Throwable;
1335

1436
}

src/main/java/org/digitalsmile/gpio/core/file/FileDescriptor.java

Lines changed: 56 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,11 @@
77
import java.lang.invoke.MethodHandle;
88
import java.util.Arrays;
99

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

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

32+
/**
33+
* Forbids creating an instance of this class.
34+
*/
35+
private FileDescriptor() {
36+
}
37+
38+
/**
39+
* Opens file at path with selected flag ({@link Flag}).
40+
*
41+
* @param path - the file to open
42+
* @param openFlag - flag to handle with file ({@link Flag})
43+
* @return file descriptor if file is successfully open
44+
*/
2745
public static int open(String path, int openFlag) {
2846
logger.trace("Opening {}", path);
2947
var fd = 0;
3048
try (Arena offHeap = Arena.ofConfined()) {
3149
var str = offHeap.allocateUtf8String(path);
3250
fd = (int) OPEN64.invoke(str, openFlag);
51+
if (fd < 0) {
52+
throw new RuntimeException("File " + path + " is not readable!");
53+
}
3354
} catch (Throwable e) {
3455
throw new RuntimeException(e);
3556
}
36-
if (fd == -1) {
37-
throw new RuntimeException("File " + path + " is not readable!");
38-
}
3957
logger.trace("Opened {} with file descriptor {}", path, fd);
4058
return fd;
4159
}
4260

43-
public static int close(int fd) {
61+
/**
62+
* Opens file at path with flag {@link Flag}
63+
*
64+
* @param path - the file to open
65+
* @return file descriptor if file is successfully open
66+
*/
67+
public static int open(String path) {
68+
return open(path, Flag.O_RDWR);
69+
}
70+
71+
72+
/**
73+
* Closes given file descriptor.
74+
*
75+
* @param fd - file descriptor to close
76+
*/
77+
public static void close(int fd) {
4478
logger.trace("Closing file descriptor {}", fd);
4579
try {
4680
var result = (int) CLOSE.invoke(fd);
81+
if (result < 0) {
82+
throw new RuntimeException("Cannot close file with descriptor " + fd);
83+
}
4784
logger.trace("Closed file descriptor with result {}", result);
48-
return result;
4985
} catch (Throwable e) {
5086
throw new RuntimeException(e);
5187
}
5288
}
5389

54-
55-
56-
public static int open(String path) {
57-
return open(path, Flags.O_RDWR);
58-
}
59-
90+
/**
91+
* Reads file descriptor with predefined size.
92+
*
93+
* @param fd - file descriptor to read
94+
* @param size - size of the byte buffer to read into
95+
* @return byte array with contents of the read file descriptor
96+
*/
6097
public static byte[] read(int fd, int size) {
6198
logger.trace("Reading file descriptor {}", fd);
62-
var byteResult = new byte[]{};
99+
var byteResult = new byte[size];
63100
try (Arena offHeap = Arena.ofConfined()) {
64-
var bufferMemorySegment = offHeap.allocateArray(ValueLayout.JAVA_BYTE, new byte[size]);
101+
var bufferMemorySegment = offHeap.allocateArray(ValueLayout.JAVA_BYTE, byteResult);
65102
var read = (int) READ.invoke(fd, bufferMemorySegment, size);
66103
if (read != size) {
67104
throw new RuntimeException("Read " + read + " bytes, but size was " + size);
@@ -75,6 +112,12 @@ public static byte[] read(int fd, int size) {
75112
return byteResult;
76113
}
77114

115+
/**
116+
* Writes byte array data to the provided file descriptor.
117+
*
118+
* @param fd - file descriptor to write
119+
* @param data - byte array of data to write
120+
*/
78121
public static void write(int fd, byte[] data) {
79122
logger.trace("Writing to file descriptor {} with data {}", fd, Arrays.toString(data));
80123
try (Arena offHeap = Arena.ofConfined()) {
@@ -89,5 +132,4 @@ public static void write(int fd, byte[] data) {
89132
}
90133
logger.trace("Wrote to file descriptor {}", fd);
91134
}
92-
93135
}

src/main/java/org/digitalsmile/gpio/core/file/Flags.java renamed to src/main/java/org/digitalsmile/gpio/core/file/Flag.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
package org.digitalsmile.gpio.core.file;
22

3-
public final class Flags {
3+
/**
4+
* Kernel flags for open command.
5+
*/
6+
public final class Flag {
47

58
public static final int O_APPEND = 1024;
69
public static final int O_ASYNC = 8192;

src/main/java/org/digitalsmile/gpio/core/ioctl/Command.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,16 @@
11
package org.digitalsmile.gpio.core.ioctl;
22

3+
/**
4+
* Commands to be provided for ioctl calls.
5+
*/
36
public final class Command {
47

8+
/**
9+
* Forbids creating an instance of this class.
10+
*/
11+
private Command() {
12+
}
13+
514
public static long getGpioGetChipInfoIoctl() {
615
return Internals.GPIO_GET_CHIPINFO_IOCTL;
716
}

0 commit comments

Comments
 (0)