Skip to content

Commit 2fde37a

Browse files
Merge pull request #393 from oss-slu/118-shift-register-class
Add ShiftRegister74HC595Helper for SPI-based shift register control
2 parents 4e7f775 + ab1cd33 commit 2fde37a

2 files changed

Lines changed: 217 additions & 0 deletions

File tree

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
package com.opensourcewithslu.outputdevices;
2+
3+
import java.io.IOException;
4+
import java.util.Objects;
5+
6+
import org.slf4j.Logger;
7+
import org.slf4j.LoggerFactory;
8+
9+
import com.pi4j.io.spi.Spi;
10+
11+
/**
12+
* Helper for controlling a 74HC595 shift register over SPI (Pi4J).
13+
*
14+
* Provides simple operations to write a full byte to the shift register,
15+
* set or clear individual output bits, clear the register,
16+
* and read the last written state. All SPI communication is performed using
17+
* the provided {@link com.pi4j.io.spi.Spi} instance.
18+
*
19+
* The helper maintains an internal state byte reflecting the last value
20+
* written to the device.
21+
*
22+
*/
23+
public class ShiftRegister74HC595Helper {
24+
25+
private static final Logger log = LoggerFactory.getLogger(ShiftRegister74HC595Helper.class);
26+
private final Spi spi;
27+
private byte state = 0x00;
28+
29+
/**
30+
* Constructs a new ShiftRegister74HC595Helper with the specified SPI interface.
31+
*
32+
* Initializes the helper with a clean state (0x00) and sets up logging.
33+
*
34+
*
35+
* @param spi the SPI interface to use for communication with the shift register
36+
* @throws NullPointerException if spi is null
37+
*/
38+
public ShiftRegister74HC595Helper(Spi spi) {
39+
this.spi = Objects.requireNonNull(spi, "spi must not be null");
40+
log.info("74HC595 helper initialized; state=0x00");
41+
}
42+
43+
/**
44+
* Shifts out a byte value to the 74HC595 shift register via SPI.
45+
*
46+
* This method writes the specified byte value to the shift register and
47+
* updates the internal state tracking. The operation is logged for debugging.
48+
*
49+
* @param value the byte value to shift out to the register
50+
* @throws IOException if the SPI write operation fails
51+
*/
52+
public void shiftOut(byte value) throws IOException {
53+
try {
54+
spi.write(value);
55+
this.state = value;
56+
log.info("Shifted out 0x{}", toHex(this.state));
57+
} catch (Exception e) {
58+
String msg = String.format("SPI write failed for 0x%s: %s", toHex(value), e.getMessage());
59+
log.error(msg, e);
60+
throw new IOException(msg, e);
61+
}
62+
}
63+
64+
/**
65+
* Clears the shift register by setting all outputs to LOW (0x00).
66+
*
67+
* This is equivalent to calling {@code shiftOut((byte) 0x00)}.
68+
*
69+
* @throws IOException if the SPI write operation fails
70+
*/
71+
public void clear() throws IOException {
72+
shiftOut((byte) 0x00);
73+
log.info("Shift register cleared");
74+
}
75+
76+
/**
77+
* Sets or clears a specific bit in the shift register.
78+
*
79+
* This method modifies the internal state by setting or clearing the specified
80+
* bit index (0-7), then shifts out the updated state to the register.
81+
*
82+
* @param bitIndex the index of the bit to modify (0-7, where 0 is LSB)
83+
* @param value {@code true} to set the bit to HIGH, {@code false} to set to LOW
84+
* @throws IllegalArgumentException if bitIndex is not in the range 0-7
85+
* @throws IOException if the SPI write operation fails
86+
*/
87+
public void setBit(int bitIndex, boolean value) throws IOException {
88+
if (bitIndex < 0 || bitIndex > 7) {
89+
throw new IllegalArgumentException("bitIndex must be 0..7; got " + bitIndex);
90+
}
91+
if (value) {
92+
state |= (1 << bitIndex);
93+
} else {
94+
state &= ~(1 << bitIndex);
95+
}
96+
shiftOut(state);
97+
log.info("Bit {} set to {}", bitIndex, value);
98+
}
99+
100+
/**
101+
* Clears (sets to LOW) a specific bit in the shift register.
102+
*
103+
* This is a convenience method equivalent to calling {@code setBit(bitIndex, false)}.
104+
*
105+
* @param bitIndex the index of the bit to clear (0-7, where 0 is LSB)
106+
* @throws IllegalArgumentException if bitIndex is not in the range 0-7
107+
* @throws IOException if the SPI write operation fails
108+
*/
109+
public void clearBit(int bitIndex) throws IOException {
110+
setBit(bitIndex, false);
111+
log.info("Bit {} cleared", bitIndex);
112+
}
113+
114+
/**
115+
* Returns the current state of the shift register.
116+
*
117+
* This method returns the internally tracked state without performing
118+
* any SPI operations. The state reflects the last value written to
119+
* the shift register.
120+
*
121+
* @return the current state as a byte value
122+
*/
123+
public byte getState() {
124+
return state;
125+
}
126+
127+
/**
128+
* Converts a byte value to its hexadecimal string representation.
129+
*
130+
* This is a utility method used for logging purposes to display
131+
* byte values in a readable hexadecimal format.
132+
*
133+
* @param b the byte value to convert
134+
* @return a two-character uppercase hexadecimal string representation
135+
*/
136+
private static String toHex(byte b) {
137+
return String.format("%02X", b & 0xFF);
138+
}
139+
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
package com.opensourcewithslu.outputdevices;
2+
3+
import static org.junit.jupiter.api.Assertions.*;
4+
import static org.mockito.Mockito.*;
5+
6+
import java.io.IOException;
7+
8+
import org.junit.jupiter.api.BeforeEach;
9+
import org.junit.jupiter.api.Test;
10+
import org.mockito.Mock;
11+
import org.mockito.MockitoAnnotations;
12+
13+
import com.pi4j.io.spi.Spi;
14+
15+
public class ShiftRegister74HC595HelperTest {
16+
17+
@Mock
18+
private Spi spi;
19+
20+
private ShiftRegister74HC595Helper helper;
21+
22+
@BeforeEach
23+
public void setUp() {
24+
MockitoAnnotations.openMocks(this);
25+
helper = new ShiftRegister74HC595Helper(spi);
26+
}
27+
28+
@Test
29+
public void testShiftOut_ValidByte() throws IOException {
30+
byte value = 0x5A;
31+
helper.shiftOut(value);
32+
verify(spi, times(1)).write(value);
33+
}
34+
35+
@Test
36+
public void testShiftOut_SpiException() throws IOException {
37+
byte value = 0x5A;
38+
doThrow(new RuntimeException("SPI error")).when(spi).write(value);
39+
IOException exception = assertThrows(IOException.class, () -> helper.shiftOut(value));
40+
assertTrue(exception.getMessage().contains("SPI write failed"));
41+
}
42+
43+
@Test
44+
public void testClear() throws IOException {
45+
helper.clear();
46+
verify(spi, times(1)).write((byte) 0x00);
47+
}
48+
49+
@Test
50+
public void testSetBit_ValidIndex() throws IOException {
51+
helper.setBit(3, true);
52+
verify(spi, times(1)).write((byte) 0x08);
53+
helper.setBit(3, false);
54+
verify(spi, times(1)).write((byte) 0x00);
55+
}
56+
57+
@Test
58+
public void testSetBit_InvalidIndex() {
59+
IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> helper.setBit(8, true));
60+
assertEquals("bitIndex must be 0..7; got 8", exception.getMessage());
61+
}
62+
63+
@Test
64+
public void testClearBit() throws IOException {
65+
helper.setBit(2, true); // Set bit 2 first
66+
verify(spi, times(1)).write((byte) 0x04);
67+
helper.clearBit(2);
68+
verify(spi, times(1)).write((byte) 0x00);
69+
}
70+
71+
@Test
72+
public void testGetState() throws IOException {
73+
assertEquals(0x00, helper.getState());
74+
helper.setBit(1, true);
75+
assertEquals(0x02, helper.getState());
76+
}
77+
78+
}

0 commit comments

Comments
 (0)