diff --git a/components/src/main/java/com/opensourcewithslu/components/controllers/FourDigitSevenSegmentDisplayController.java b/components/src/main/java/com/opensourcewithslu/components/controllers/FourDigitSevenSegmentDisplayController.java new file mode 100644 index 00000000..14ec7c79 --- /dev/null +++ b/components/src/main/java/com/opensourcewithslu/components/controllers/FourDigitSevenSegmentDisplayController.java @@ -0,0 +1,44 @@ +package com.opensourcewithslu.components.controllers; + +import com.opensourcewithslu.outputdevices.FourDigitSevenSegmentDisplayHelper; +import com.pi4j.io.gpio.digital.DigitalOutput; +import io.micronaut.http.annotation.Controller; +import io.micronaut.http.annotation.Get; +import jakarta.inject.Named; + +//tag::ex[] +@Controller("/four-digit-seven-segment") +public class FourDigitSevenSegmentDisplayController { + private final FourDigitSevenSegmentDisplayHelper displayHelper; + + public FourDigitSevenSegmentDisplayController(@Named("sdi") DigitalOutput sdi, + @Named("rclk") DigitalOutput rclk, + @Named("srclk") DigitalOutput srclk, + @Named("digit-0") DigitalOutput digit0, + @Named("digit-1") DigitalOutput digit1, + @Named("digit-2") DigitalOutput digit2, + @Named("digit-3") DigitalOutput digit3) { + this.displayHelper = new FourDigitSevenSegmentDisplayHelper(sdi, rclk, srclk, digit0, digit1, digit2, digit3); + } + + @Get("/enable") + public void enable() { + displayHelper.enable(); + } + + @Get("/disable") + public void disable() { + displayHelper.disable(); + } + + @Get("/displayValue/{value}") + public void displayValue(String value) { + displayHelper.displayValue(value); + } + + @Get("/clear") + public void clearDisplay() { + displayHelper.clear(); + } +} +//end::ex[] diff --git a/components/src/main/resources/application.yml b/components/src/main/resources/application.yml index 9e27c6d2..3de3df04 100644 --- a/components/src/main/resources/application.yml +++ b/components/src/main/resources/application.yml @@ -35,7 +35,7 @@ pi4j: shutdown: 0 servo-motor: name: Servo Motor - address: 17 + address: 18 pwmType: SOFTWARE provider: pigpio-pwm initial: 0 @@ -166,6 +166,48 @@ pi4j: shutdown: LOW initial: LOW provider: pigpio-digital-output + digit-0: + name: Digit 0 + address: 17 + shutdown: LOW + initial: LOW + provider: pigpio-digital-output + digit-1: + name: Digit 1 + address: 27 + shutdown: LOW + initial: LOW + provider: pigpio-digital-output + digit-2: + name: Digit 2 + address: 22 + shutdown: LOW + initial: LOW + provider: pigpio-digital-output + digit-3: + name: Digit 3 + address: 10 + shutdown: LOW + initial: LOW + provider: pigpio-digital-output + sdi: + name: SDI + address: 24 + shutdown: LOW + initial: LOW + provider: pigpio-digital-output + rclk: + name: RCLK + address: 23 + shutdown: LOW + initial: LOW + provider: pigpio-digital-output + srclk: + name: SRCLK + address: 18 + shutdown: LOW + initial: LOW + provider: pigpio-digital-output # end::digitalOutput[] # tag::multiInput[] diff --git a/pi4micronaut-utils/src/docs/asciidoc/components/outputComponents/fourDigitSevenSegment.adoc b/pi4micronaut-utils/src/docs/asciidoc/components/outputComponents/fourDigitSevenSegment.adoc new file mode 100644 index 00000000..e4f69753 --- /dev/null +++ b/pi4micronaut-utils/src/docs/asciidoc/components/outputComponents/fourDigitSevenSegment.adoc @@ -0,0 +1,118 @@ +:imagesdir: img/ + +ifndef::rootpath[] +:rootpath: ../../ +endif::rootpath[] + +ifdef::rootpath[] +:imagesdir: {rootpath}{imagesdir} +endif::rootpath[] + +==== 4-Digit 7-Segment Display + +[.text-right] +https://github.com/oss-slu/Pi4Micronaut/edit/develop/pi4micronaut-utils/src/docs/asciidoc/components/outputComponents/fourDigitSevenSegment.adoc[Improve this doc] + +===== Overview + +This section provides details of the 4-digit 7-segment display, including its components and assembly instructions. + +===== Components + +* 1 x Raspberry Pi +* 1 x Breadboard +* 1 x T-Extension Board +* 25 x Jumper Wire +* 4 x Resistor (220Ω) +* 1 x 4-Digit 7-Segment Display +* 1 x 74HC595 +* Power source (appropriate voltage, typically 3.3V) + +===== Assembly Instructions + +* Connect the ground (GND) pins of the Raspberry Pi to the ground rails on the breadboard. +* Connect the two ground rails of the breadboard with a jumper wire. +* Place the 74HC595 on the breadboard. +* Place the four resistors on the breadboard. +* Place the 4-digit 7-segment display on the breadboard. +* Connect a jumper wire from each "digit" pin of the 4-digit 7-segment to one of the resistors. +* Connect the other end of the resistors to the Raspberry Pi's pins: + +- Digit 1 to GPIO17 (BCM pin 18) +- Digit 2 to GPIO27 (BCM pin 27) +- Digit 3 to GPIO22 (BCM pin 22) +- Digit 4 to SPIMOSI (BCM pin 10) + +// TODO: Describe connections to 74HC595 + +===== Circuit Diagram + +image::four_digit_circuit.webp[] + +===== Schematic Diagram + +image::four_digit_schematic.webp[] + +===== Functionality + +The display can be enabled/disabled, display a custom value, or cleared. + +Each digit of the display can display a digit 0 to 9, an uppercase letter A to F, a hypen (-), or a blank space. +Each of the four decimal points can also be turned on or off. + +Example possible values include: + +* "1" (displayed with the 1 in the first digit and the others blank) +* "8.8.8.8." (displayed with all segments enabled as `8.8.8.8.`) +* "A.-.42" (displayed as ```A.-.42```) + +===== Testing + +Use the below commands to test the component. +This will cause the display to turn on and display the value `1234`. + +[source,bash] +---- +$ curl http://localhost:8080/four-digit-seven-segment/enable +$ curl http://localhost:8080/four-digit-seven-segment/displayValue/1234 +---- + +* `/enable` - Enables the display. +* `/disable` - Disables the display. +* `/displayValue/{value}` - Displays a custom value on the display. +* `/clear` - Clears the display. + +===== Troubleshooting + +* Display does not turn on +- Ensure all connections are secure and correct. +- Check the 74HC595 for proper orientation and placement. +- Ensure the software configuration matches the hardware setup. +- Look for any error messages in the console or logs. + +* Display turns on but does not respond to commands +- Check the software configuration for any discrepancies. +- Ensure the 74HC595 is functioning properly. + +===== YAML Configuration + +[source,yaml] +---- +i2c: +four-digit-seven-segment: + name: 4 Digit 7 Segment Display + bus: 1 + device: 0x27 +---- + +===== Constructor and Methods + +To see the constructor and methods of our FourDigitSevenSegmentHelper class see our javadoc link:https://oss-slu.github.io/Pi4Micronaut/javadoc/com/opensourcewithslu/outputdevices/FourDigitSevenSegmentHelper.html[here] +for more details. + +===== An Example Controller + +[source,java] +---- +include::../../../../../../components/src/main/java/com/opensourcewithslu/components/controllers/FourDigitSevenSegmentDisplayController.java[tag=ex] +---- diff --git a/pi4micronaut-utils/src/docs/asciidoc/img/four_digit_circuit.webp b/pi4micronaut-utils/src/docs/asciidoc/img/four_digit_circuit.webp new file mode 100644 index 00000000..8761985a Binary files /dev/null and b/pi4micronaut-utils/src/docs/asciidoc/img/four_digit_circuit.webp differ diff --git a/pi4micronaut-utils/src/docs/asciidoc/img/four_digit_schematic.webp b/pi4micronaut-utils/src/docs/asciidoc/img/four_digit_schematic.webp new file mode 100644 index 00000000..2b981db2 Binary files /dev/null and b/pi4micronaut-utils/src/docs/asciidoc/img/four_digit_schematic.webp differ diff --git a/pi4micronaut-utils/src/main/java/com/opensourcewithslu/outputdevices/FourDigitSevenSegmentDisplayHelper.java b/pi4micronaut-utils/src/main/java/com/opensourcewithslu/outputdevices/FourDigitSevenSegmentDisplayHelper.java new file mode 100644 index 00000000..3d8ca8b7 --- /dev/null +++ b/pi4micronaut-utils/src/main/java/com/opensourcewithslu/outputdevices/FourDigitSevenSegmentDisplayHelper.java @@ -0,0 +1,254 @@ +package com.opensourcewithslu.outputdevices; + +import com.pi4j.io.gpio.digital.DigitalOutput; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Map; + +public class FourDigitSevenSegmentDisplayHelper { + private static Logger log = LoggerFactory.getLogger(FourDigitSevenSegmentDisplayHelper.class); + + private final DigitalOutput sdi; + private final DigitalOutput rclk; + private final DigitalOutput srclk; + private final DigitalOutput digit0; + private final DigitalOutput digit1; + private final DigitalOutput digit2; + private final DigitalOutput digit3; + + private String displayValue; + private boolean enabled = false; + private final int decimalPoint = 0x7f; + + /** + * Mapping of characters to their respective byte representation. + * Each byte is a bitset where each bit specifies if a specific segment should be disabled (1) or enabled (0). + * Note carefully that the bits are inverted, so 0 means enabled and 1 means disabled. + */ + protected static final Map CHAR_BITSETS = Map.ofEntries( + Map.entry(' ', 0xff), + Map.entry('-', 0x02), + Map.entry('0', 0xc0), + Map.entry('1', 0xf9), + Map.entry('2', 0xa4), + Map.entry('3', 0xb0), + Map.entry('4', 0x99), + Map.entry('5', 0x92), + Map.entry('6', 0x82), + Map.entry('7', 0xf8), + Map.entry('8', 0x80), + Map.entry('9', 0x90), + Map.entry('A', 0x88), + Map.entry('B', 0x83), + Map.entry('C', 0xc6), + Map.entry('D', 0xA1), + Map.entry('E', 0x86), + Map.entry('F', 0x8e) + ); + + /** + * Constructor for the FourDigitSevenSegmentDisplayHelper class. + * + * @param sdi Serial data input + * @param rclk Register clock + * @param srclk Shift register clock + * @param digit0 The first digit + * @param digit1 The second digit + * @param digit2 The third digit + * @param digit3 The fourth digit + */ + //tag::const[] + public FourDigitSevenSegmentDisplayHelper(DigitalOutput sdi, DigitalOutput rclk, DigitalOutput srclk, DigitalOutput digit0, DigitalOutput digit1, DigitalOutput digit2, DigitalOutput digit3) + //end::const[] + { + this.sdi = sdi; + this.rclk = rclk; + this.srclk = srclk; + this.digit0 = digit0; + this.digit1 = digit1; + this.digit2 = digit2; + this.digit3 = digit3; + + this.displayValue = ""; + } + + private void shiftOut(Integer data, boolean decimalPointEnabled) { + int value; + for (int i = 0; i < 8; i++) { // Loop through each bit in the byte, one for each of the 7 segment and the decimal point + if (decimalPointEnabled) { + value = (data & (1 << (7 - i)) & this.decimalPoint); + } else { + value = (data & (1 << (7 - i)) | ~this.decimalPoint); + } + if (value != 0) { + sdi.high(); // Disables segment + } else { + sdi.low(); // Enables segment + } + srclk.high(); + srclk.low(); + } + rclk.high(); + rclk.low(); + } + + /** + * Clears the display. + */ + //tag::method[] + public void clear() + //end::method[] + { + displayValue = ""; + } + + public void setDigit(int digit, char c, boolean decimalPoint) { + digit0.low(); + digit1.low(); + digit2.low(); + digit3.low(); + + switch (digit) { + case 0: + digit0.high(); + break; + case 1: + digit1.high(); + break; + case 2: + digit2.high(); + break; + case 3: + digit3.high(); + break; + } + + // Lookup byte value for given character + final var value = CHAR_BITSETS.get(Character.toUpperCase(c)); + if (value == null) { + throw new IllegalArgumentException("Character is not supported by seven-segment display"); + } + + try { + shiftOut(value, decimalPoint); + } catch (Exception e) { + log.error(String.format("Error shifting out value %d to digit %d:", value, digit), e); + } + } + + /** + * Enables the display. + */ + //tag::method[] + public void enable() + //end::method[] + { + this.enabled = true; + this.startDisplayThread(); + } + + /** + * Disables the display. + */ + //tag::method[] + public void disable() + //end::method[] + { + this.clear(); + this.enabled = false; + } + + /** + * Displays a value on the four-digit seven-segment display. + * + * @param value The value to display. It can include digits 0-9, letters A-F (case-insensitive), + * hyphens, spaces, and decimal points. The value must not have more than 4 non-decimal + * point characters, no consecutive decimal points, and if there are 4 non-decimal + * point characters, decimal points must not appear on the ends. + */ + //tag::method[] + public void displayValue(String value) + //end::method[] + { + // Parse out the decimal points + String noDecimals = value.replaceAll("\\.", ""); + + // Check: No more than 4 non-decimal point characters long + if (noDecimals.length() > 4) { + log.error("Display value must not have more than 4 non-decimal point characters"); + return; + } + + // Check: No consecutive decimal points + if (value.contains("..")) { + log.error("Display value cannot have consecutive decimal points"); + return; + } + + // Check: If there are 4 non-decimal point characters, then decimal points must not appear on the ends + if (noDecimals.length() == 4 && (value.startsWith(".") || value.endsWith("."))) { + log.error("Display value must have decimal points appearing strictly between the digits"); + return; + } + + // Check: Non-decimal point characters must be digits 0 to 1, letters A to F (case-insensitive), -, or space + String valid = "1234567890ABCDEFabcdef- "; + for (char character : noDecimals.toCharArray()) { + if (valid.indexOf(character) == -1) { + log.error("Each display value digit must be numeric, a letter A to F (case insensitive), a hyphen, or a space"); + return; + } + } + + value = value.toUpperCase(); + this.displayValue = value; + log.info("Displaying value: {}", value); + } + + private void startDisplayThread() { + Thread displayThread = new Thread(() -> { + while (enabled) { + for (int i = 0; i < 4; i++) { + if (i < displayValue.length()) { + char value = displayValue.charAt(i); + setDigit(i, value, true); + } else { + setDigit(i, '0', false); + } + try { + Thread.sleep(5); // Adjust the delay as needed + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + log.error("Display thread interrupted", e); + } + } + } + }); + displayThread.start(); + } + + /** + * Sets the logger object. + * + * @param log Logger object to set the logger to. + */ + //tag::method[] + public void setLog(Logger log) + //end::method[] + { + FourDigitSevenSegmentDisplayHelper.log = log; + } + + /** + * Gets the display value. + * + * @return The display value + */ + //tag::method[] + public String getDisplayValue() + //end::method[] + { + return displayValue; + } +} diff --git a/pi4micronaut-utils/src/test/java/com/opensourcewithslu/outputdevices/FourDigitSevenSegmentDisplayHelperTest.java b/pi4micronaut-utils/src/test/java/com/opensourcewithslu/outputdevices/FourDigitSevenSegmentDisplayHelperTest.java new file mode 100644 index 00000000..ac58f40a --- /dev/null +++ b/pi4micronaut-utils/src/test/java/com/opensourcewithslu/outputdevices/FourDigitSevenSegmentDisplayHelperTest.java @@ -0,0 +1,266 @@ +package com.opensourcewithslu.outputdevices; + +import com.pi4j.context.ContextProperties; +import com.pi4j.io.gpio.digital.DigitalOutput; +import com.pi4j.io.i2c.I2CConfig; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.slf4j.Logger; + +import static org.mockito.Mockito.*; +import static org.junit.jupiter.api.Assertions.*; + +import com.pi4j.context.Context; +import com.pi4j.crowpi.components.SevenSegmentComponent; + +import java.lang.reflect.Field; + +public class FourDigitSevenSegmentDisplayHelperTest { + DigitalOutput sdi = mock(DigitalOutput.class); + DigitalOutput rclk = mock(DigitalOutput.class); + DigitalOutput srclk = mock(DigitalOutput.class); + DigitalOutput digit0 = mock(DigitalOutput.class); + DigitalOutput digit1 = mock(DigitalOutput.class); + DigitalOutput digit2 = mock(DigitalOutput.class); + DigitalOutput digit3 = mock(DigitalOutput.class); + FourDigitSevenSegmentDisplayHelper displayHelper = new FourDigitSevenSegmentDisplayHelper(sdi, rclk, srclk, digit0, digit1, digit2, digit3); + Logger log = mock(Logger.class); + + /*@BeforeEach + void setUp() throws Exception { + // Mock the Context to return a non-null ContextProperties + when(pi4jContext.properties()).thenReturn(contextProperties); + + displayHelper = new FourDigitSevenSegmentDisplayHelper(i2cConfig, pi4jContext); + + // Use reflection to set the mock SevenSegmentComponent + Field displayField = FourDigitSevenSegmentDisplayHelper.class.getDeclaredField("display"); + displayField.setAccessible(true); + displayField.set(displayHelper, displayComponent); + }*/ + + @BeforeEach + public void openMocks() { + displayHelper.setLog(log); + } + + @Test + void longNumberFails() { + String value = "12345"; + displayHelper.displayValue(value); + verify(log).error("Display value must not have more than 4 non-decimal point characters"); + verify(log, never()).info("Displaying value: {}", "12345"); + } + + @Test + void consecutiveDecimalPointsFails() { + String value = "1..23"; + displayHelper.displayValue(value); + verify(log).error("Display value cannot have consecutive decimal points"); + verify(log, never()).info("Displaying value: {}", "1..23"); + } + + @Test + void decimalPointOnLeftEndFails() { + String value = ".1234"; + displayHelper.displayValue(value); + verify(log).error("Display value must have decimal points appearing strictly between the digits"); + verify(log, never()).info("Displaying value: {}", ".1234"); + } + + @Test + void decimalPointOnRightEndFails() { + String value = "1234."; + displayHelper.displayValue(value); + verify(log).error("Display value must have decimal points appearing strictly between the digits"); + verify(log, never()).info("Displaying value: {}", "1234."); + } + + @Test + void decimalPointsOnBothEndsFails() { + String value = ".1234."; + displayHelper.displayValue(value); + verify(log).error("Display value must have decimal points appearing strictly between the digits"); + verify(log, never()).info("Displaying value: {}", ".1234."); + } + + @Test + void invalidCharacterFails() { + String value = "G"; + displayHelper.displayValue(value); + verify(log).error("Each display value digit must be numeric, a letter A to F (case insensitive), a hyphen, or a space"); + verify(log, never()).info("Displaying value: {}", "G"); + } + + @Test + void displaysLetters() { + String value = "ABCD"; + displayHelper.displayValue(value); + verify(log).info("Displaying value: {}", "ABCD"); + + String displayed = displayHelper.getDisplayValue(); + assertEquals("ABCD", displayed); + } + + @Test + void displaysLettersWithLowercase() { + String value = "abcd"; + displayHelper.displayValue(value); + verify(log).info("Displaying value: {}", "ABCD"); + + String displayed = displayHelper.getDisplayValue(); + assertEquals("ABCD", displayed); + } + + @Test + void displaysLettersWithMixedCase() { + String value = "aBcD"; + displayHelper.displayValue(value); + verify(log).info("Displaying value: {}", "ABCD"); + + String displayed = displayHelper.getDisplayValue(); + assertEquals("ABCD", displayed); + } + + @Test + void displaysHyphen() { + String value = "-"; + displayHelper.displayValue(value); + verify(log).info("Displaying value: {}", "-"); + + String displayed = displayHelper.getDisplayValue(); + assertEquals("-", displayed); + } + + @Test + void displaysSpaces() { + String value = "2 2"; + displayHelper.displayValue(value); + verify(log).info("Displaying value: {}", "2 2"); + + String displayed = displayHelper.getDisplayValue(); + assertEquals("2 2", displayed); + } + + @Test + void displaysNegativeNumber() { + String number = "-123"; + displayHelper.displayValue(number); + verify(log).info("Displaying value: {}", "-123"); + + String displayed = displayHelper.getDisplayValue(); + assertEquals("-123", displayed); + } + + @Test + void displaysFourDigitNumber() { + String number = "1234"; + displayHelper.displayValue(number); + verify(log).info("Displaying value: {}", "1234"); + + String displayed = displayHelper.getDisplayValue(); + assertEquals("1234", displayed); + } + + @Test + void displaysThreeDigitNumber() { + String number = "123"; + displayHelper.displayValue(number); + verify(log).info("Displaying value: {}", "123"); + + String displayed = displayHelper.getDisplayValue(); + assertEquals("123", displayed); + } + + @Test + void displaysTwoDigitNumber() { + String number = "34"; + displayHelper.displayValue(number); + verify(log).info("Displaying value: {}", "34"); + + String displayed = displayHelper.getDisplayValue(); + assertEquals("34", displayed); + } + + @Test + void displaysOneDigitNumber() { + String number = "4"; + displayHelper.displayValue(number); + verify(log).info("Displaying value: {}", "4"); + + String displayed = displayHelper.getDisplayValue(); + assertEquals("4", displayed); + } + + @Test + void displaysBlankValue() { + String number = ""; + displayHelper.displayValue(number); + verify(log).info("Displaying value: {}", ""); + + String displayed = displayHelper.getDisplayValue(); + assertEquals("", displayed); + } + + @Test + void displaysDecimalNumber() { + String number = "1.23"; + displayHelper.displayValue(number); + verify(log).info("Displaying value: {}", "1.23"); + + String displayed = displayHelper.getDisplayValue(); + assertEquals("1.23", displayed); + } + + @Test + void displaysDecimalNumberWithLeadingDecimal() { + String number = ".23"; + displayHelper.displayValue(number); + verify(log).info("Displaying value: {}", ".23"); + + String displayed = displayHelper.getDisplayValue(); + assertEquals(".23", displayed); + } + + @Test + void displaysDecimalNumberWithTrailingDecimal() { + String number = "1."; + displayHelper.displayValue(number); + verify(log).info("Displaying value: {}", "1."); + + String displayed = displayHelper.getDisplayValue(); + assertEquals("1.", displayed); + } + + @Test + void displaysMultipleDecimals() { + String number = "1.2.3.4"; + displayHelper.displayValue(number); + verify(log).info("Displaying value: {}", "1.2.3.4"); + + String displayed = displayHelper.getDisplayValue(); + assertEquals("1.2.3.4", displayed); + } + + @Test + void displaysSpacesAndDecimals() { + String number = " . . . "; + displayHelper.displayValue(number); + verify(log).info("Displaying value: {}", " . . . "); + + String displayed = displayHelper.getDisplayValue(); + assertEquals(" . . . ", displayed); + } + + @Test + void clearDisplay() { + String number = "1234"; + displayHelper.displayValue(number); + + displayHelper.clear(); + + String displayed = displayHelper.getDisplayValue(); + assertEquals("", displayed); + } +}