Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
package com.opensourcewithslu.outputdevices;

import java.io.IOException;
import java.util.Objects;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.pi4j.io.spi.Spi;

/**
* Helper for controlling a 74HC595 shift register over SPI (Pi4J).
*
* Provides simple operations to write a full byte to the shift register,
* set or clear individual output bits, clear the register,
* and read the last written state. All SPI communication is performed using
* the provided {@link com.pi4j.io.spi.Spi} instance.
*
* The helper maintains an internal state byte reflecting the last value
* written to the device.
*
*/
public class ShiftRegister74HC595Helper {

private static final Logger log = LoggerFactory.getLogger(ShiftRegister74HC595Helper.class);
private final Spi spi;
private byte state = 0x00;

/**
* Constructs a new ShiftRegister74HC595Helper with the specified SPI interface.
*
* Initializes the helper with a clean state (0x00) and sets up logging.
*
*
* @param spi the SPI interface to use for communication with the shift register
* @throws NullPointerException if spi is null
*/
public ShiftRegister74HC595Helper(Spi spi) {
this.spi = Objects.requireNonNull(spi, "spi must not be null");
log.info("74HC595 helper initialized; state=0x00");
}

/**
* Shifts out a byte value to the 74HC595 shift register via SPI.
*
* This method writes the specified byte value to the shift register and
* updates the internal state tracking. The operation is logged for debugging.
*
* @param value the byte value to shift out to the register
* @throws IOException if the SPI write operation fails
*/
public void shiftOut(byte value) throws IOException {
try {
spi.write(value);
this.state = value;
log.info("Shifted out 0x{}", toHex(this.state));
} catch (Exception e) {
String msg = String.format("SPI write failed for 0x%s: %s", toHex(value), e.getMessage());
log.error(msg, e);
throw new IOException(msg, e);
}
}

/**
* Clears the shift register by setting all outputs to LOW (0x00).
*
* This is equivalent to calling {@code shiftOut((byte) 0x00)}.
*
* @throws IOException if the SPI write operation fails
*/
public void clear() throws IOException {
shiftOut((byte) 0x00);
log.info("Shift register cleared");
}

/**
* Sets or clears a specific bit in the shift register.
*
* This method modifies the internal state by setting or clearing the specified
* bit index (0-7), then shifts out the updated state to the register.
*
* @param bitIndex the index of the bit to modify (0-7, where 0 is LSB)
* @param value {@code true} to set the bit to HIGH, {@code false} to set to LOW
* @throws IllegalArgumentException if bitIndex is not in the range 0-7
* @throws IOException if the SPI write operation fails
*/
public void setBit(int bitIndex, boolean value) throws IOException {
if (bitIndex < 0 || bitIndex > 7) {
throw new IllegalArgumentException("bitIndex must be 0..7; got " + bitIndex);
}
if (value) {
state |= (1 << bitIndex);
} else {
state &= ~(1 << bitIndex);
}
shiftOut(state);
log.info("Bit {} set to {}", bitIndex, value);
}

/**
* Clears (sets to LOW) a specific bit in the shift register.
*
* This is a convenience method equivalent to calling {@code setBit(bitIndex, false)}.
*
* @param bitIndex the index of the bit to clear (0-7, where 0 is LSB)
* @throws IllegalArgumentException if bitIndex is not in the range 0-7
* @throws IOException if the SPI write operation fails
*/
public void clearBit(int bitIndex) throws IOException {
setBit(bitIndex, false);
log.info("Bit {} cleared", bitIndex);
}

/**
* Returns the current state of the shift register.
*
* This method returns the internally tracked state without performing
* any SPI operations. The state reflects the last value written to
* the shift register.
*
* @return the current state as a byte value
*/
public byte getState() {
return state;
}

/**
* Converts a byte value to its hexadecimal string representation.
*
* This is a utility method used for logging purposes to display
* byte values in a readable hexadecimal format.
*
* @param b the byte value to convert
* @return a two-character uppercase hexadecimal string representation
*/
private static String toHex(byte b) {
return String.format("%02X", b & 0xFF);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package com.opensourcewithslu.outputdevices;

import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;

import java.io.IOException;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;

import com.pi4j.io.spi.Spi;

public class ShiftRegister74HC595HelperTest {

@Mock
private Spi spi;

private ShiftRegister74HC595Helper helper;

@BeforeEach
public void setUp() {
MockitoAnnotations.openMocks(this);
helper = new ShiftRegister74HC595Helper(spi);
}

@Test
public void testShiftOut_ValidByte() throws IOException {
byte value = 0x5A;
helper.shiftOut(value);
verify(spi, times(1)).write(value);
}

@Test
public void testShiftOut_SpiException() throws IOException {
byte value = 0x5A;
doThrow(new RuntimeException("SPI error")).when(spi).write(value);
IOException exception = assertThrows(IOException.class, () -> helper.shiftOut(value));
assertTrue(exception.getMessage().contains("SPI write failed"));
}

@Test
public void testClear() throws IOException {
helper.clear();
verify(spi, times(1)).write((byte) 0x00);
}

@Test
public void testSetBit_ValidIndex() throws IOException {
helper.setBit(3, true);
verify(spi, times(1)).write((byte) 0x08);
helper.setBit(3, false);
verify(spi, times(1)).write((byte) 0x00);
}

@Test
public void testSetBit_InvalidIndex() {
IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> helper.setBit(8, true));
assertEquals("bitIndex must be 0..7; got 8", exception.getMessage());
}

@Test
public void testClearBit() throws IOException {
helper.setBit(2, true); // Set bit 2 first
verify(spi, times(1)).write((byte) 0x04);
helper.clearBit(2);
verify(spi, times(1)).write((byte) 0x00);
}

@Test
public void testGetState() throws IOException {
assertEquals(0x00, helper.getState());
helper.setBit(1, true);
assertEquals(0x02, helper.getState());
}

}
Loading