[wpilib] Add LED pattern API for easily animating addressable LEDs (#6344)

Add LEDReader and LEDWriter helper interfaces to facilitate composing simple patterns into more complex ones, e.g. LEDPattern.solid(Color.kBlue).breathe(Seconds.of(0.75)). Pattern composition relies on changing out the write behavior; for example, offsetBy increments the indexes to write to; while blink will switch between playing a base pattern and turning off all the LEDs.

Add a view class for splitting a single large buffer into smaller distinct sections, which is useful for dealing with long chained LED strips mounted on different parts of a robot. Views cannot be written directly to an LED strip (in fact, trying to do so won't even compile).

Adds some utility methods to the Color class for interpolating between two colors, and support color representations with 32-bit integers to avoid object allocations.

Co-authored-by: Tyler Veness <calcmogul@gmail.com>
This commit is contained in:
Sam Carlberg
2024-06-05 22:41:10 -04:00
committed by GitHub
parent 5221069bcc
commit ad18fa62ee
19 changed files with 3610 additions and 311 deletions

View File

@@ -0,0 +1,69 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
package edu.wpi.first.wpilibj;
import static org.junit.jupiter.api.Assertions.assertEquals;
import edu.wpi.first.wpilibj.util.Color;
import org.junit.jupiter.api.Test;
class AddressableLEDBufferViewTest {
@Test
void singleLED() {
var buffer = new AddressableLEDBuffer(10);
var view = new AddressableLEDBufferView(buffer, 5, 5);
var color = Color.kAqua;
view.setLED(0, color);
assertEquals(color, buffer.getLED(5));
assertEquals(color, view.getLED(0));
}
@Test
void segment() {
var buffer = new AddressableLEDBuffer(10);
var view = new AddressableLEDBufferView(buffer, 2, 8);
view.setLED(0, Color.kAqua);
assertEquals(Color.kAqua, buffer.getLED(2));
view.setLED(6, Color.kAzure);
assertEquals(Color.kAzure, buffer.getLED(8));
}
@Test
void manualReversed() {
var buffer = new AddressableLEDBuffer(10);
var view = new AddressableLEDBufferView(buffer, 8, 2);
// LED 0 in the view should write to LED 8 on the real buffer
view.setLED(0, Color.kAqua);
assertEquals(Color.kAqua, buffer.getLED(8));
// .. and LED 6 in the view should write to LED 2 on the real buffer
view.setLED(6, Color.kAzure);
assertEquals(Color.kAzure, buffer.getLED(2));
}
@Test
void fullManualReversed() {
var buffer = new AddressableLEDBuffer(10);
var view = new AddressableLEDBufferView(buffer, 9, 0);
view.setLED(0, Color.kWhite);
assertEquals(Color.kWhite, buffer.getLED(9));
buffer.setLED(8, Color.kRed);
assertEquals(Color.kRed, view.getLED(1));
}
@Test
void reversed() {
var buffer = new AddressableLEDBuffer(10);
var view = new AddressableLEDBufferView(buffer, 0, 9).reversed();
view.setLED(0, Color.kWhite);
assertEquals(Color.kWhite, buffer.getLED(9));
view.setLED(9, Color.kRed);
assertEquals(Color.kRed, buffer.getLED(0));
}
}

View File

@@ -0,0 +1,798 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
package edu.wpi.first.wpilibj;
import static edu.wpi.first.units.Units.Centimeters;
import static edu.wpi.first.units.Units.MetersPerSecond;
import static edu.wpi.first.units.Units.Microsecond;
import static edu.wpi.first.units.Units.Microseconds;
import static edu.wpi.first.units.Units.Percent;
import static edu.wpi.first.units.Units.Seconds;
import static edu.wpi.first.units.Units.Value;
import static edu.wpi.first.wpilibj.util.Color.kBlack;
import static edu.wpi.first.wpilibj.util.Color.kBlue;
import static edu.wpi.first.wpilibj.util.Color.kLime;
import static edu.wpi.first.wpilibj.util.Color.kMagenta;
import static edu.wpi.first.wpilibj.util.Color.kMidnightBlue;
import static edu.wpi.first.wpilibj.util.Color.kPurple;
import static edu.wpi.first.wpilibj.util.Color.kRed;
import static edu.wpi.first.wpilibj.util.Color.kWhite;
import static edu.wpi.first.wpilibj.util.Color.kYellow;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.fail;
import edu.wpi.first.util.WPIUtilJNI;
import edu.wpi.first.wpilibj.util.Color;
import edu.wpi.first.wpilibj.util.Color8Bit;
import java.util.Map;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
class LEDPatternTest {
// Applies a pattern of White, Yellow, Purple to LED triplets
LEDPattern m_whiteYellowPurple =
(reader, writer) -> {
for (int led = 0; led < reader.getLength(); led++) {
switch (led % 3) {
case 0:
writer.setLED(led, kWhite);
break;
case 1:
writer.setLED(led, kYellow);
break;
case 2:
writer.setLED(led, kPurple);
break;
default:
fail("Bad test setup");
break;
}
}
};
@BeforeEach
void setUp() {
WPIUtilJNI.enableMockTime();
WPIUtilJNI.setMockTime(0L);
}
@AfterEach
void tearDown() {
WPIUtilJNI.setMockTime(0L);
WPIUtilJNI.disableMockTime();
}
@Test
void solidColor() {
LEDPattern pattern = LEDPattern.solid(kYellow);
AddressableLEDBuffer buffer = new AddressableLEDBuffer(99);
pattern.applyTo(buffer);
for (int i = 0; i < buffer.getLength(); i++) {
assertEquals(kYellow, buffer.getLED(i));
}
}
@Test
void gradient0SetsToBlack() {
LEDPattern pattern = LEDPattern.gradient();
AddressableLEDBuffer buffer = new AddressableLEDBuffer(99);
for (int i = 0; i < buffer.getLength(); i++) {
buffer.setRGB(i, 127, 128, 129);
}
pattern.applyTo(buffer);
for (int i = 0; i < buffer.getLength(); i++) {
assertEquals(kBlack, buffer.getLED(i));
}
}
@Test
void gradient1SetsToSolid() {
LEDPattern pattern = LEDPattern.gradient(kYellow);
AddressableLEDBuffer buffer = new AddressableLEDBuffer(99);
pattern.applyTo(buffer);
for (int i = 0; i < buffer.getLength(); i++) {
assertEquals(kYellow, buffer.getLED(i));
}
}
@Test
void gradient2Colors() {
LEDPattern pattern = LEDPattern.gradient(kYellow, kPurple);
AddressableLEDBuffer buffer = new AddressableLEDBuffer(99);
pattern.applyTo(buffer);
assertColorEquals(kYellow, buffer.getLED(0));
assertColorEquals(Color.lerpRGB(kYellow, kPurple, 25 / 49.0), buffer.getLED(25));
assertColorEquals(kPurple, buffer.getLED(49));
assertColorEquals(Color.lerpRGB(kYellow, kPurple, 25 / 49.0), buffer.getLED(73));
assertColorEquals(kYellow, buffer.getLED(98));
}
@Test
void gradient3Colors() {
LEDPattern pattern = LEDPattern.gradient(kYellow, kPurple, kWhite);
AddressableLEDBuffer buffer = new AddressableLEDBuffer(99);
pattern.applyTo(buffer);
assertColorEquals(kYellow, buffer.getLED(0));
assertColorEquals(Color.lerpRGB(kYellow, kPurple, 25.0 / 33.0), buffer.getLED(25));
assertColorEquals(kPurple, buffer.getLED(33));
assertColorEquals(Color.lerpRGB(kPurple, kWhite, 25.0 / 33.0), buffer.getLED(58));
assertColorEquals(kWhite, buffer.getLED(66));
assertColorEquals(Color.lerpRGB(kWhite, kYellow, 25.0 / 33.0), buffer.getLED(91));
assertColorEquals(Color.lerpRGB(kWhite, kYellow, 32.0 / 33.0), buffer.getLED(98));
}
@Test
void step0SetsToBlack() {
LEDPattern pattern = LEDPattern.steps(Map.of());
AddressableLEDBuffer buffer = new AddressableLEDBuffer(99);
for (int i = 0; i < buffer.getLength(); i++) {
buffer.setRGB(i, 127, 128, 129);
}
pattern.applyTo(buffer);
for (int i = 0; i < 99; i++) {
assertColorEquals(kBlack, buffer.getLED(i));
}
}
@Test
void step1SetsToSolid() {
LEDPattern pattern = LEDPattern.steps(Map.of(0.0, kYellow));
AddressableLEDBuffer buffer = new AddressableLEDBuffer(99);
pattern.applyTo(buffer);
for (int i = 0; i < 99; i++) {
assertColorEquals(kYellow, buffer.getLED(i));
}
}
@Test
void step1HalfSetsToHalfOffHalfColor() {
LEDPattern pattern = LEDPattern.steps(Map.of(0.50, kYellow));
AddressableLEDBuffer buffer = new AddressableLEDBuffer(99);
pattern.applyTo(buffer);
// [0, 48] should be black...
for (int i = 0; i < 49; i++) {
assertColorEquals(kBlack, buffer.getLED(i));
}
// ... and [49, <end>] should be the color that was set
for (int i = 49; i < buffer.getLength(); i++) {
assertColorEquals(kYellow, buffer.getLED(i));
}
}
@Test
void scrollForward() {
var buffer = new AddressableLEDBuffer(256);
LEDPattern base =
(reader, writer) -> {
for (int led = 0; led < reader.getLength(); led++) {
writer.setRGB(led, led % 256, led % 256, led % 256);
}
};
// scroll forwards 1/256th (1 LED) per microsecond - this makes mock time easier
var scroll = base.scrollAtRelativeSpeed(Value.per(Microsecond).of(1 / 256.0));
for (int time = 0; time < 500; time++) {
WPIUtilJNI.setMockTime(time);
scroll.applyTo(buffer);
for (int led = 0; led < buffer.getLength(); led++) {
// Base: [(0, 0, 0) (1, 1, 1) (2, 2, 2) (3, 3, 3) (4, 4, 4) ... (255, 255, 255)]
// Value for every channel should DECREASE by 1 in each timestep, wrapping around 0 and 255
// t=0, channel value = (0, 1, 2, ..., 254, 255)
// t=1, channel value = (255, 0, 1, ..., 253, 254)
// t=2, channel value = (254, 255, 0, ..., 252, 253)
// t=255, channel value = (1, 2, 3, ..., 255, 0)
// t=256, channel value = (0, 1, 2, ..., 254, 255)
int ch = Math.floorMod(led - time, 256);
assertEquals(new Color8Bit(ch, ch, ch), buffer.getLED8Bit(led));
}
}
}
@Test
void scrollBackward() {
var buffer = new AddressableLEDBuffer(256);
LEDPattern base =
(reader, writer) -> {
for (int led = 0; led < reader.getLength(); led++) {
writer.setRGB(led, led % 256, led % 256, led % 256);
}
};
// scroll backwards 1/256th (1 LED) per microsecond - this makes mock time easier
var scroll = base.scrollAtRelativeSpeed(Value.per(Microsecond).of(-1 / 256.0));
for (int time = 0; time < 500; time++) {
WPIUtilJNI.setMockTime(time);
scroll.applyTo(buffer);
for (int led = 0; led < buffer.getLength(); led++) {
// Base: [(0, 0, 0) (1, 1, 1) (2, 2, 2) (3, 3, 3) (4, 4, 4) ... (255, 255, 255)]
// Value for every channel should INCREASE by 1 in each timestep, wrapping around 0 and 255
// t=0, channel value = (0, 1, 2, ..., 254, 255)
// t=1, channel value = (1, 2, 3, ..., 255, 0)
// t=2, channel value = (2, 3, 4, ..., 0, 1)
// t=255, channel value = (255, 0, 1, ..., 253, 254)
// t=256, channel value = (0, 1, 2, ..., 254, 255)
int ch = Math.floorMod(led + time, 256);
assertEquals(new Color8Bit(ch, ch, ch), buffer.getLED8Bit(led));
}
}
}
@Test
void scrollAbsoluteSpeedForward() {
var buffer = new AddressableLEDBuffer(256);
LEDPattern base =
(reader, writer) -> {
for (int led = 0; led < reader.getLength(); led++) {
writer.setRGB(led, led % 256, led % 256, led % 256);
}
};
// scroll at 16 m/s, LED spacing = 2cm
// buffer is 256 LEDs, so total length = 512cm = 5.12m
// scrolling at 16 m/s yields a period of 0.32 seconds, or 0.00125 seconds per LED (800 LEDs/s)
var scroll = base.scrollAtAbsoluteSpeed(MetersPerSecond.of(16), Centimeters.of(2));
for (int time = 0; time < 500; time++) {
WPIUtilJNI.setMockTime(time * 1_250); // 1.25ms per LED
scroll.applyTo(buffer);
for (int led = 0; led < buffer.getLength(); led++) {
// Base: [(0, 0, 0) (1, 1, 1) (2, 2, 2) (3, 3, 3) (4, 4, 4) ... (255, 255, 255)]
// Value for every channel should DECREASE by 1 in each timestep, wrapping around 0 and 255
// t=0, channel value = (0, 1, 2, ..., 254, 255)
// t=1, channel value = (255, 0, 1, ..., 253, 254)
// t=2, channel value = (254, 255, 0, ..., 252, 253)
// t=255, channel value = (1, 2, 3, ..., 255, 0)
// t=256, channel value = (0, 1, 2, ..., 254, 255)
int ch = Math.floorMod(led - time, 256);
assertEquals(new Color8Bit(ch, ch, ch), buffer.getLED8Bit(led));
}
}
}
@Test
void scrollAbsoluteSpeedBackward() {
var buffer = new AddressableLEDBuffer(256);
LEDPattern base =
(reader, writer) -> {
for (int led = 0; led < reader.getLength(); led++) {
writer.setRGB(led, led % 256, led % 256, led % 256);
}
};
// scroll at 16 m/s, LED spacing = 2cm
// buffer is 256 LEDs, so total length = 512cm = 5.12m
// scrolling at 16 m/s yields a period of 0.32 seconds, or 0.00125 seconds per LED (800 LEDs/s)
var scroll = base.scrollAtAbsoluteSpeed(MetersPerSecond.of(-16), Centimeters.of(2));
for (int time = 0; time < 500; time++) {
WPIUtilJNI.setMockTime(time * 1_250); // 1.25ms per LED
scroll.applyTo(buffer);
for (int led = 0; led < buffer.getLength(); led++) {
// Base: [(0, 0, 0) (1, 1, 1) (2, 2, 2) (3, 3, 3) (4, 4, 4) ... (255, 255, 255)]
// Value for every channel should DECREASE by 1 in each timestep, wrapping around 0 and 255
// t=0, channel value = (0, 1, 2, ..., 254, 255)
// t=1, channel value = (255, 0, 1, ..., 253, 254)
// t=2, channel value = (254, 255, 0, ..., 252, 253)
// t=255, channel value = (1, 2, 3, ..., 255, 0)
// t=256, channel value = (0, 1, 2, ..., 254, 255)
int ch = Math.floorMod(led + time, 256);
assertEquals(new Color8Bit(ch, ch, ch), buffer.getLED8Bit(led));
}
}
}
@Test
void rainbowAtFullSize() {
var buffer = new AddressableLEDBuffer(180);
int saturation = 255;
int value = 255;
var pattern = LEDPattern.rainbow(saturation, value);
pattern.applyTo(buffer);
for (int led = 0; led < buffer.getLength(); led++) {
assertColorEquals(Color.fromHSV(led, saturation, value), buffer.getLED(led));
}
}
@Test
void rainbowAtHalfSize() {
var buffer = new AddressableLEDBuffer(90);
int saturation = 42;
int value = 87;
var pattern = LEDPattern.rainbow(saturation, value);
pattern.applyTo(buffer);
for (int led = 0; led < buffer.getLength(); led++) {
assertColorEquals(Color.fromHSV(led * 2, saturation, value), buffer.getLED(led));
}
}
@Test
void rainbowAtOneThirdSize() {
var buffer = new AddressableLEDBuffer(60);
int saturation = 191;
int value = 255;
var pattern = LEDPattern.rainbow(saturation, value);
pattern.applyTo(buffer);
for (int led = 0; led < buffer.getLength(); led++) {
assertColorEquals(Color.fromHSV(led * 3, saturation, value), buffer.getLED(led));
}
}
@Test
void rainbowAtDoubleSize() {
var buffer = new AddressableLEDBuffer(360);
int saturation = 212;
int value = 93;
var pattern = LEDPattern.rainbow(saturation, value);
pattern.applyTo(buffer);
for (int led = 0; led < buffer.getLength(); led++) {
assertColorEquals(Color.fromHSV(led / 2, saturation, value), buffer.getLED(led));
}
}
@Test
void rainbowOddSize() {
var buffer = new AddressableLEDBuffer(127);
double scale = 180.0 / buffer.getLength();
int saturation = 73;
int value = 128;
var pattern = LEDPattern.rainbow(saturation, value);
pattern.applyTo(buffer);
for (int led = 0; led < buffer.getLength(); led++) {
assertColorEquals(Color.fromHSV((int) (led * scale), saturation, value), buffer.getLED(led));
}
}
@Test
void reverseSolid() {
var buffer = new AddressableLEDBuffer(90);
var pattern = LEDPattern.solid(Color.kRosyBrown).reversed();
pattern.applyTo(buffer);
for (int led = 0; led < buffer.getLength(); led++) {
assertColorEquals(Color.kRosyBrown, buffer.getLED(led));
}
}
@Test
void reverseSteps() {
var buffer = new AddressableLEDBuffer(100);
var pattern = LEDPattern.steps(Map.of(0, kWhite, 0.5, kYellow)).reversed();
pattern.applyTo(buffer);
// colors should be swapped; yellow first, then white
for (int led = 0; led < buffer.getLength(); led++) {
if (led < 50) {
assertColorEquals(kYellow, buffer.getLED(led));
} else {
assertColorEquals(kWhite, buffer.getLED(led));
}
}
}
@Test
void offsetPositive() {
var buffer = new AddressableLEDBuffer(21);
// offset repeats PWY
var offset = m_whiteYellowPurple.offsetBy(1);
offset.applyTo(buffer);
for (int led = 0; led < buffer.getLength(); led++) {
Color color = buffer.getLED(led);
switch (led % 3) {
case 0:
assertColorEquals(kPurple, color);
break;
case 1:
assertColorEquals(kWhite, color);
break;
case 2:
assertColorEquals(kYellow, color);
break;
default:
fail("Bad test setup");
break;
}
}
}
@Test
void offsetNegative() {
var buffer = new AddressableLEDBuffer(21);
// offset repeats YPW
var offset = m_whiteYellowPurple.offsetBy(-1);
offset.applyTo(buffer);
for (int led = 0; led < buffer.getLength(); led++) {
Color color = buffer.getLED(led);
switch (led % 3) {
case 0:
assertColorEquals(kYellow, color);
break;
case 1:
assertColorEquals(kPurple, color);
break;
case 2:
assertColorEquals(kWhite, color);
break;
default:
fail("Bad test setup");
break;
}
}
}
@Test
void offsetZero() {
var buffer = new AddressableLEDBuffer(21);
// offset copies the base pattern, WYP
var offset = m_whiteYellowPurple.offsetBy(0);
offset.applyTo(buffer);
for (int led = 0; led < buffer.getLength(); led++) {
Color color = buffer.getLED(led);
switch (led % 3) {
case 0:
assertColorEquals(kWhite, color);
break;
case 1:
assertColorEquals(kYellow, color);
break;
case 2:
assertColorEquals(kPurple, color);
break;
default:
fail("Bad test setup");
break;
}
}
}
@Test
void blinkSymmetric() {
// on for 2 seconds, off for 2 seconds
var pattern = LEDPattern.solid(kWhite).blink(Seconds.of(2));
var buffer = new AddressableLEDBuffer(1);
for (int t = 0; t < 8; t++) {
WPIUtilJNI.setMockTime(t * 1_000_000L); // time travel 1 second
pattern.applyTo(buffer);
Color color = buffer.getLED(0);
switch (t) {
case 0:
case 1:
case 4:
case 5:
assertColorEquals(kWhite, color);
break;
case 2:
case 3:
case 6:
case 7:
assertColorEquals(kBlack, color);
break;
default:
fail("Bad test setup");
break;
}
}
}
@Test
void blinkAsymmetric() {
// on for 3 seconds, off for 1 second
var pattern = LEDPattern.solid(kWhite).blink(Seconds.of(3), Seconds.of(1));
var buffer = new AddressableLEDBuffer(1);
for (int t = 0; t < 8; t++) {
WPIUtilJNI.setMockTime(t * 1_000_000L); // time travel 1 second
pattern.applyTo(buffer);
Color color = buffer.getLED(0);
switch (t) {
case 0:
case 1:
case 2: // first period
case 4:
case 5:
case 6: // second period
assertColorEquals(kWhite, color);
break;
case 3:
case 7:
assertColorEquals(kBlack, color);
break;
default:
fail("Bad test setup");
break;
}
}
}
@Test
void blinkInSync() {
AtomicBoolean condition = new AtomicBoolean(false);
var pattern = LEDPattern.solid(kWhite).synchronizedBlink(condition::get);
var buffer = new AddressableLEDBuffer(1);
pattern.applyTo(buffer);
assertColorEquals(kBlack, buffer.getLED(0));
condition.set(true);
pattern.applyTo(buffer);
assertColorEquals(kWhite, buffer.getLED(0));
condition.set(false);
pattern.applyTo(buffer);
assertColorEquals(kBlack, buffer.getLED(0));
}
@Test
void breathe() {
final Color midGray = new Color(0.5, 0.5, 0.5);
var pattern = LEDPattern.solid(kWhite).breathe(Microseconds.of(4));
var buffer = new AddressableLEDBuffer(1);
{
WPIUtilJNI.setMockTime(0); // start
pattern.applyTo(buffer);
assertColorEquals(kWhite, buffer.getLED(0));
}
{
WPIUtilJNI.setMockTime(1); // midway (down)
pattern.applyTo(buffer);
assertColorEquals(midGray, buffer.getLED(0));
}
{
WPIUtilJNI.setMockTime(2); // bottom
pattern.applyTo(buffer);
assertColorEquals(kBlack, buffer.getLED(0));
}
{
WPIUtilJNI.setMockTime(3); // midway (up)
pattern.applyTo(buffer);
assertColorEquals(midGray, buffer.getLED(0));
}
{
WPIUtilJNI.setMockTime(4); // back to start
pattern.applyTo(buffer);
assertColorEquals(kWhite, buffer.getLED(0));
}
}
@Test
void overlaySolidOnSolid() {
var overlay = LEDPattern.solid(kYellow).overlayOn(LEDPattern.solid(kWhite));
var buffer = new AddressableLEDBuffer(1);
overlay.applyTo(buffer);
assertColorEquals(kYellow, buffer.getLED(0));
}
@Test
void overlayNearlyBlack() {
Color overlayColor = new Color(new Color8Bit(1, 0, 0));
var overlay = LEDPattern.solid(overlayColor).overlayOn(LEDPattern.solid(kWhite));
var buffer = new AddressableLEDBuffer(1);
overlay.applyTo(buffer);
assertColorEquals(overlayColor, buffer.getLED(0));
}
@Test
void overlayMixed() {
var overlay =
LEDPattern.steps(Map.of(0, kYellow, 0.5, kBlack)).overlayOn(LEDPattern.solid(kWhite));
var buffer = new AddressableLEDBuffer(2);
overlay.applyTo(buffer);
assertColorEquals(kYellow, buffer.getLED(0));
assertColorEquals(kWhite, buffer.getLED(1));
}
@Test
void blend() {
var pattern1 = LEDPattern.solid(kBlue);
var pattern2 = LEDPattern.solid(kRed);
var blend = pattern1.blend(pattern2);
var buffer = new AddressableLEDBuffer(1);
blend.applyTo(buffer);
// Individual RGB channels are averaged; #0000FF blended with #FF0000 yields #7F007F
assertColorEquals(new Color(127, 0, 127), buffer.getLED(0));
}
@Test
void binaryMask() {
Color color = new Color(123, 123, 123);
var base = LEDPattern.solid(color);
// first 50% mask on, last 50% mask off
var mask = LEDPattern.steps(Map.of(0, kWhite, 0.5, kBlack));
var masked = base.mask(mask);
var buffer = new AddressableLEDBuffer(10);
masked.applyTo(buffer);
for (int i = 0; i < 5; i++) {
assertColorEquals(color, buffer.getLED(i));
}
for (int i = 5; i < 10; i++) {
assertColorEquals(kBlack, buffer.getLED(i));
}
}
@Test
void channelwiseMask() {
Color baseColor = new Color(123, 123, 123);
Color halfGray = new Color(0.5, 0.5, 0.5);
var base = LEDPattern.solid(baseColor);
var mask =
LEDPattern.steps(Map.of(0, kRed, 0.2, kLime, 0.4, kBlue, 0.6, halfGray, 0.8, kWhite));
var masked = base.mask(mask);
var buffer = new AddressableLEDBuffer(5);
masked.applyTo(buffer);
assertColorEquals(new Color(123, 0, 0), buffer.getLED(0)); // red channel only
assertColorEquals(new Color(0, 123, 0), buffer.getLED(1)); // green channel only
assertColorEquals(new Color(0, 0, 123), buffer.getLED(2)); // blue channel only
// mask channels are all 0b00111111, base is 0b00111011,
// so the AND should give us the unmodified base color
assertColorEquals(baseColor, buffer.getLED(3));
assertColorEquals(baseColor, buffer.getLED(4)); // full color allowed
}
@Test
void progressMaskLayer() {
var progress = new AtomicReference<>(0.0);
var maskLayer = LEDPattern.progressMaskLayer(progress::get);
var buffer = new AddressableLEDBuffer(100);
for (double t = 0; t <= 1.0; t += 0.01) {
progress.set(t);
maskLayer.applyTo(buffer);
int lastMaskedLED = (int) (t * 100);
for (int i = 0; i < lastMaskedLED; i++) {
assertColorEquals(
kWhite,
buffer.getLED(i),
"Progress " + lastMaskedLED + "%, LED " + i + " should be WHITE");
}
for (int i = lastMaskedLED; i < 100; i++) {
assertColorEquals(
kBlack,
buffer.getLED(i),
"Progress " + lastMaskedLED + "% , LED " + i + " should be BLACK");
}
}
}
@Test
void zeroBrightness() {
var pattern = LEDPattern.solid(kRed).atBrightness(Percent.zero());
var buffer = new AddressableLEDBuffer(1);
pattern.applyTo(buffer);
assertColorEquals(kBlack, buffer.getLED(0));
}
@Test
void sameBrightness() {
var pattern = LEDPattern.solid(kMagenta).atBrightness(Percent.of(100));
var buffer = new AddressableLEDBuffer(1);
pattern.applyTo(buffer);
assertColorEquals(kMagenta, buffer.getLED(0));
}
@Test
void higherBrightness() {
var pattern = LEDPattern.solid(kMagenta).atBrightness(Value.of(4 / 3.0));
var buffer = new AddressableLEDBuffer(1);
pattern.applyTo(buffer);
assertColorEquals(kMagenta, buffer.getLED(0));
}
@Test
void negativeBrightness() {
var pattern = LEDPattern.solid(kWhite).atBrightness(Percent.of(-1000));
var buffer = new AddressableLEDBuffer(1);
pattern.applyTo(buffer);
assertColorEquals(kBlack, buffer.getLED(0));
}
@Test
void clippingBrightness() {
var pattern = LEDPattern.solid(kMidnightBlue).atBrightness(Percent.of(10000));
var buffer = new AddressableLEDBuffer(1);
pattern.applyTo(buffer);
assertColorEquals(kWhite, buffer.getLED(0));
}
void assertColorEquals(Color expected, Color actual) {
assertEquals(new Color8Bit(expected), new Color8Bit(actual));
}
void assertColorEquals(Color expected, Color actual, String message) {
assertEquals(new Color8Bit(expected), new Color8Bit(actual), message);
}
}

View File

@@ -4,10 +4,16 @@
package edu.wpi.first.wpilibj.util;
import static org.junit.jupiter.api.Assertions.assertAll;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.params.provider.Arguments.arguments;
import java.util.stream.Stream;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
class ColorTest {
@Test
@@ -82,4 +88,39 @@ class ColorTest {
assertEquals("#FF8040", color.toHexString());
assertEquals("#FF8040", color.toString());
}
@ParameterizedTest
@MethodSource("hsvToRgbProvider")
void hsvToRgb(int h, int s, int v, int r, int g, int b) {
int rgb = Color.hsvToRgb(h, s, v);
int R = Color.unpackRGB(rgb, Color.RGBChannel.kRed);
int G = Color.unpackRGB(rgb, Color.RGBChannel.kGreen);
int B = Color.unpackRGB(rgb, Color.RGBChannel.kBlue);
assertAll(
() -> assertEquals(r, R, "R value didn't match"),
() -> assertEquals(g, G, "G value didn't match"),
() -> assertEquals(b, B, "B value didn't match"));
}
private static Stream<Arguments> hsvToRgbProvider() {
return Stream.of(
arguments(0, 0, 0, 0, 0, 0), // Black
arguments(0, 0, 255, 255, 255, 255), // White
arguments(0, 255, 255, 255, 0, 0), // Red
arguments(60, 255, 255, 0, 255, 0), // Lime
arguments(120, 255, 255, 0, 0, 255), // Blue
arguments(30, 255, 255, 255, 255, 0), // Yellow
arguments(90, 255, 255, 0, 255, 255), // Cyan
arguments(150, 255, 255, 255, 0, 255), // Magenta
arguments(0, 0, 191, 191, 191, 191), // Silver
arguments(0, 0, 128, 128, 128, 128), // Gray
arguments(0, 255, 128, 128, 0, 0), // Maroon
arguments(30, 255, 128, 128, 128, 0), // Olive
arguments(60, 255, 128, 0, 128, 0), // Green
arguments(150, 255, 128, 128, 0, 128), // Purple
arguments(90, 255, 128, 0, 128, 128), // Teal
arguments(120, 255, 128, 0, 0, 128) // Navy
);
}
}