// 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. #include "wpi/hardware/led/LEDPattern.hpp" #include #include "wpi/math/util/MathUtil.hpp" #include "wpi/util/MathExtras.hpp" #include "wpi/util/timestamp.h" namespace wpi { static LEDPattern whiteYellowPurple{[](auto data, auto writer) { for (size_t led = 0; led < data.size(); led++) { switch (led % 3) { case 0: writer(led, wpi::util::Color::WHITE); break; case 1: writer(led, wpi::util::Color::YELLOW); break; case 2: writer(led, wpi::util::Color::PURPLE); break; } } }}; void AssertIndexColor(std::span data, int index, wpi::util::Color color); wpi::util::Color LerpColors(wpi::util::Color a, wpi::util::Color b, double t); TEST(LEDPatternTest, SolidColor) { LEDPattern pattern = LEDPattern::Solid(wpi::util::Color::YELLOW); std::array buffer; // prefill for (int i = 0; i < 5; i++) { buffer[i].SetLED(wpi::util::Color::PURPLE); } pattern.ApplyTo(buffer); for (int i = 0; i < 5; i++) { AssertIndexColor(buffer, i, wpi::util::Color::YELLOW); } } TEST(LEDPatternTest, EmptyGradientSetsToBlack) { std::array colors; LEDPattern pattern = LEDPattern::Gradient(LEDPattern::GradientType::CONTINUOUS, colors); std::array buffer; pattern.ApplyTo(buffer); for (int i = 0; i < 5; i++) { AssertIndexColor(buffer, i, wpi::util::Color::BLACK); } } TEST(LEDPatternTest, SingleColorGradientSetsSolid) { std::array colors{wpi::util::Color::YELLOW}; LEDPattern pattern = LEDPattern::Gradient(LEDPattern::GradientType::CONTINUOUS, colors); std::array buffer; pattern.ApplyTo(buffer); for (int i = 0; i < 5; i++) { AssertIndexColor(buffer, i, wpi::util::Color::YELLOW); } } TEST(LEDPatternTest, Gradient2Colors) { std::array colors{wpi::util::Color::YELLOW, wpi::util::Color::PURPLE}; LEDPattern pattern = LEDPattern::Gradient(LEDPattern::GradientType::CONTINUOUS, colors); std::array buffer; pattern.ApplyTo(buffer); AssertIndexColor(buffer, 0, wpi::util::Color::YELLOW); AssertIndexColor(buffer, 25, LerpColors(wpi::util::Color::YELLOW, wpi::util::Color::PURPLE, 25 / 49.0)); AssertIndexColor(buffer, 49, wpi::util::Color::PURPLE); AssertIndexColor(buffer, 74, LerpColors(wpi::util::Color::PURPLE, wpi::util::Color::YELLOW, 25 / 49.0)); AssertIndexColor(buffer, 98, wpi::util::Color::YELLOW); } TEST(LEDPatternTest, DiscontinuousGradient2Colors) { std::array colors{wpi::util::Color::YELLOW, wpi::util::Color::PURPLE}; LEDPattern pattern = LEDPattern::Gradient(LEDPattern::GradientType::DISCONTINUOUS, colors); std::array buffer; pattern.ApplyTo(buffer); AssertIndexColor(buffer, 0, wpi::util::Color::YELLOW); AssertIndexColor( buffer, 49, LerpColors(wpi::util::Color::YELLOW, wpi::util::Color::PURPLE, 0.5)); AssertIndexColor(buffer, 98, wpi::util::Color::PURPLE); } TEST(LEDPatternTest, Gradient3Colors) { std::array colors{wpi::util::Color::YELLOW, wpi::util::Color::PURPLE, wpi::util::Color::WHITE}; LEDPattern pattern = LEDPattern::Gradient(LEDPattern::GradientType::CONTINUOUS, colors); std::array buffer; pattern.ApplyTo(buffer); AssertIndexColor(buffer, 0, wpi::util::Color::YELLOW); AssertIndexColor(buffer, 25, LerpColors(wpi::util::Color::YELLOW, wpi::util::Color::PURPLE, 25 / 33.0)); AssertIndexColor(buffer, 33, wpi::util::Color::PURPLE); AssertIndexColor( buffer, 58, LerpColors(wpi::util::Color::PURPLE, wpi::util::Color::WHITE, 25 / 33.0)); AssertIndexColor(buffer, 66, wpi::util::Color::WHITE); AssertIndexColor( buffer, 91, LerpColors(wpi::util::Color::WHITE, wpi::util::Color::YELLOW, 25 / 33.0)); AssertIndexColor( buffer, 98, LerpColors(wpi::util::Color::WHITE, wpi::util::Color::YELLOW, 32 / 33.0)); } TEST(LEDPatternTest, DiscontinuousGradient3Colors) { std::array colors{wpi::util::Color::YELLOW, wpi::util::Color::PURPLE, wpi::util::Color::WHITE}; LEDPattern pattern = LEDPattern::Gradient(LEDPattern::GradientType::DISCONTINUOUS, colors); std::array buffer; pattern.ApplyTo(buffer); AssertIndexColor(buffer, 0, wpi::util::Color::YELLOW); AssertIndexColor( buffer, 25, LerpColors(wpi::util::Color::YELLOW, wpi::util::Color::PURPLE, 0.5)); AssertIndexColor(buffer, 50, wpi::util::Color::PURPLE); AssertIndexColor( buffer, 75, LerpColors(wpi::util::Color::PURPLE, wpi::util::Color::WHITE, 0.5)); AssertIndexColor(buffer, 100, wpi::util::Color::WHITE); } TEST(LEDPatternTest, EmptyStepsSetsToBlack) { std::array, 0> steps; LEDPattern pattern = LEDPattern::Steps(steps); std::array buffer; // prefill for (int i = 0; i < 5; i++) { buffer[i].SetLED(wpi::util::Color::PURPLE); } pattern.ApplyTo(buffer); for (int i = 0; i < 5; i++) { AssertIndexColor(buffer, i, wpi::util::Color::BLACK); } } TEST(LEDPatternTest, SingleStepSetsSolid) { std::array, 1> steps{ std::pair{0.0, wpi::util::Color::YELLOW}}; LEDPattern pattern = LEDPattern::Steps(steps); std::array buffer; pattern.ApplyTo(buffer); for (int i = 0; i < 5; i++) { AssertIndexColor(buffer, i, wpi::util::Color::YELLOW); } } TEST(LEDPatternTest, SingleHalfStepSetsHalfOffHalfColor) { std::array, 1> steps{ std::pair{0.5, wpi::util::Color::YELLOW}}; LEDPattern pattern = LEDPattern::Steps(steps); std::array buffer; pattern.ApplyTo(buffer); // [0, 48] should be black... for (int i = 0; i < 49; i++) { AssertIndexColor(buffer, i, wpi::util::Color::BLACK); } // ... and [49, ] should be the color that was set for (int i = 49; i < 99; i++) { AssertIndexColor(buffer, i, wpi::util::Color::YELLOW); } } TEST(LEDPatternTest, ScrollRelativeForward) { // A black to white gradient LEDPattern pattern = LEDPattern{[=](auto data, auto writer) { for (size_t led = 0; led < data.size(); led++) { int ch = static_cast(led % 256); writer(led, wpi::util::Color{ch, ch, ch}); } }}; std::array buffer; // Scrolling at 1/256th of the buffer per second, // or 1 individual diode per second auto scroll = pattern.ScrollAtRelativeVelocity(wpi::units::hertz_t{1 / 256.0}); static uint64_t now = 0ull; WPI_SetNowImpl([] { return now; }); for (int time = 0; time < 500; time++) { // convert time (seconds) to microseconds now = time * 1000000ull; scroll.ApplyTo(buffer); for (size_t led = 0; led < buffer.size(); led++) { SCOPED_TRACE( fmt::format("LED {} of 256, run {} of 500", led + 1, time + 1)); // 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 = wpi::math::FloorMod(static_cast(led - time), 256); AssertIndexColor(buffer, led, wpi::util::Color{ch, ch, ch}); } } WPI_SetNowImpl(nullptr); // cleanup } TEST(LEDPatternTest, ScrollRelativeBackward) { // A black to white gradient LEDPattern pattern = LEDPattern{[=](auto data, auto writer) { for (size_t led = 0; led < data.size(); led++) { int ch = static_cast(led % 256); writer(led, wpi::util::Color{ch, ch, ch}); } }}; std::array buffer; // Scrolling at 1/256th of the buffer per second, // or 1 individual diode per second auto scroll = pattern.ScrollAtRelativeVelocity(wpi::units::hertz_t{-1 / 256.0}); static uint64_t now = 0ull; WPI_SetNowImpl([] { return now; }); for (int time = 0; time < 500; time++) { // convert time (seconds) to microseconds now = time * 1000000ull; scroll.ApplyTo(buffer); for (size_t led = 0; led < buffer.size(); led++) { SCOPED_TRACE( fmt::format("LED {} of 256, run {} of 500", led + 1, time + 1)); // 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 = wpi::math::FloorMod(static_cast(led + time), 256); AssertIndexColor(buffer, led, wpi::util::Color{ch, ch, ch}); } } WPI_SetNowImpl(nullptr); // cleanup } TEST(LEDPatternTest, ScrollAbsoluteForward) { // A black to white gradient LEDPattern pattern = LEDPattern{[](auto data, auto writer) { for (size_t led = 0; led < data.size(); led++) { int ch = static_cast(led % 256); writer(led, wpi::util::Color{ch, ch, ch}); } }}; std::array buffer; // 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) auto scroll = pattern.ScrollAtAbsoluteVelocity(16_mps, 2_cm); static uint64_t now = 0ull; WPI_SetNowImpl([] { return now; }); for (int time = 0; time < 500; time++) { // convert time (seconds) to microseconds now = time * 1250ull; // 1.25ms per LED scroll.ApplyTo(buffer); for (size_t led = 0; led < buffer.size(); led++) { SCOPED_TRACE( fmt::format("LED {} of 256, run {} of 500", led + 1, time + 1)); // 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 = wpi::math::FloorMod(static_cast(led - time), 256); AssertIndexColor(buffer, led, wpi::util::Color{ch, ch, ch}); } } WPI_SetNowImpl(nullptr); // cleanup } TEST(LEDPatternTest, ScrollAbsoluteBackward) { // A black to white gradient LEDPattern pattern = LEDPattern{[](auto data, auto writer) { for (size_t led = 0; led < data.size(); led++) { int ch = static_cast(led % 256); writer(led, wpi::util::Color{ch, ch, ch}); } }}; std::array buffer; // 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) auto scroll = pattern.ScrollAtAbsoluteVelocity(-16_mps, 2_cm); static uint64_t now = 0ull; WPI_SetNowImpl([] { return now; }); for (int time = 0; time < 500; time++) { // convert time (seconds) to microseconds now = time * 1250ull; // 1.25ms per LED scroll.ApplyTo(buffer); for (size_t led = 0; led < buffer.size(); led++) { SCOPED_TRACE( fmt::format("LED {} of 256, run {} of 500", led + 1, time + 1)); // 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 = wpi::math::FloorMod(static_cast(led + time), 256); AssertIndexColor(buffer, led, wpi::util::Color{ch, ch, ch}); } } WPI_SetNowImpl(nullptr); // cleanup } TEST(LEDPatternTest, RainbowFullSize) { std::array buffer; int saturation = 255; int value = 255; LEDPattern pattern = LEDPattern::Rainbow(saturation, value); pattern.ApplyTo(buffer); for (int led = 0; led < 180; led++) { AssertIndexColor(buffer, led, wpi::util::Color::FromHSV(led, saturation, value)); } } TEST(LEDPatternTest, LEDDataSetHSVExactRgbValues) { struct TestCase { int h; int s; int v; int r; int g; int b; }; constexpr TestCase kCases[] = { {0, 0, 0, 0, 0, 0}, {0, 0, 255, 255, 255, 255}, {0, 255, 255, 255, 0, 0}, {60, 255, 255, 0, 255, 0}, {120, 255, 255, 0, 0, 255}, {30, 255, 255, 255, 255, 0}, {90, 255, 255, 0, 255, 255}, {150, 255, 255, 255, 0, 255}, {0, 255, 128, 128, 0, 0}, {60, 255, 128, 0, 128, 0}, {120, 255, 128, 0, 0, 128}, }; for (const auto& test : kCases) { SCOPED_TRACE(::testing::Message() << "SetHSV(" << test.h << ", " << test.s << ", " << test.v << ")"); AddressableLED::LEDData data; data.SetHSV(test.h, test.s, test.v); EXPECT_EQ(test.r, data.r & 0xFF); EXPECT_EQ(test.g, data.g & 0xFF); EXPECT_EQ(test.b, data.b & 0xFF); } } TEST(LEDPatternTest, RainbowFullSizeExactRgbValues) { std::array buffer; LEDPattern::Rainbow(255, 255).ApplyTo(buffer); struct TestCase { int index; int r; int g; int b; }; constexpr TestCase kCases[] = { {0, 255, 0, 0}, {30, 255, 255, 0}, {60, 0, 255, 0}, {90, 0, 255, 255}, {120, 0, 0, 255}, {150, 255, 0, 255}, }; for (const auto& test : kCases) { SCOPED_TRACE(::testing::Message() << "LED " << test.index); EXPECT_EQ(test.r, buffer[test.index].r & 0xFF); EXPECT_EQ(test.g, buffer[test.index].g & 0xFF); EXPECT_EQ(test.b, buffer[test.index].b & 0xFF); } } TEST(LEDPatternTest, RainbowHalfSize) { std::array buffer; int saturation = 42; int value = 87; LEDPattern pattern = LEDPattern::Rainbow(saturation, value); pattern.ApplyTo(buffer); for (int led = 0; led < 90; led++) { AssertIndexColor(buffer, led, wpi::util::Color::FromHSV(led * 2, saturation, value)); } } TEST(LEDPatternTest, RainbowThirdSize) { std::array buffer; int saturation = 191; int value = 255; LEDPattern pattern = LEDPattern::Rainbow(saturation, value); pattern.ApplyTo(buffer); for (int led = 0; led < 60; led++) { SCOPED_TRACE(fmt::format("LED {} of 60", led + 1)); AssertIndexColor(buffer, led, wpi::util::Color::FromHSV(led * 3, saturation, value)); } } TEST(LEDPatternTest, RainbowDoubleSize) { std::array buffer; int saturation = 212; int value = 93; LEDPattern pattern = LEDPattern::Rainbow(saturation, value); pattern.ApplyTo(buffer); for (int led = 0; led < 360; led++) { SCOPED_TRACE(fmt::format("LED {} of 360", led + 1)); AssertIndexColor(buffer, led, wpi::util::Color::FromHSV(led / 2, saturation, value)); } } TEST(LEDPatternTest, RainbowOddSize) { std::array buffer; double scale = 180.0 / 127; int saturation = 73; int value = 128; LEDPattern pattern = LEDPattern::Rainbow(saturation, value); pattern.ApplyTo(buffer); for (int led = 0; led < 127; led++) { SCOPED_TRACE(fmt::format("LED {} of 127", led + 1)); AssertIndexColor(buffer, led, wpi::util::Color::FromHSV(static_cast(led * scale), saturation, value)); } } TEST(LEDPatternTest, ReverseSolid) { std::array buffer; const auto color = wpi::util::Color::ROSY_BROWN; auto solid = LEDPattern::Solid(color); auto pattern = solid.Reversed(); pattern.ApplyTo(buffer); for (int led = 0; led < 90; led++) { SCOPED_TRACE(fmt::format("LED {} of 90", led + 1)); AssertIndexColor(buffer, led, wpi::util::Color::ROSY_BROWN); } } TEST(LEDPatternTest, ReverseSteps) { std::array buffer; std::array, 2> steps{ std::pair{0.0, wpi::util::Color::PLUM}, std::pair{0.5, wpi::util::Color::YELLOW}}; auto stepPattern = LEDPattern::Steps(steps); auto pattern = stepPattern.Reversed(); pattern.ApplyTo(buffer); // colors should be swapped; yellow first, then plum for (int led = 0; led < 50; led++) { SCOPED_TRACE(fmt::format("LED {} of 100", led + 1)); AssertIndexColor(buffer, led, wpi::util::Color::YELLOW); } for (int led = 50; led < 100; led++) { SCOPED_TRACE(fmt::format("LED {} of 100", led + 1)); AssertIndexColor(buffer, led, wpi::util::Color::PLUM); } } TEST(LEDPatternTest, OffsetPositive) { std::array buffer; auto offset = whiteYellowPurple.OffsetBy(1); offset.ApplyTo(buffer); for (int led = 0; led < 21; led++) { SCOPED_TRACE(fmt::format("LED {} of 21", led + 1)); switch (led % 3) { case 0: AssertIndexColor(buffer, led, wpi::util::Color::PURPLE); break; case 1: AssertIndexColor(buffer, led, wpi::util::Color::WHITE); break; case 2: AssertIndexColor(buffer, led, wpi::util::Color::YELLOW); break; } } } TEST(LEDPatternTest, OffsetNegative) { std::array buffer; auto offset = whiteYellowPurple.OffsetBy(-1); offset.ApplyTo(buffer); for (int led = 0; led < 21; led++) { SCOPED_TRACE(fmt::format("LED {} of 21", led + 1)); switch (led % 3) { case 0: AssertIndexColor(buffer, led, wpi::util::Color::YELLOW); break; case 1: AssertIndexColor(buffer, led, wpi::util::Color::PURPLE); break; case 2: AssertIndexColor(buffer, led, wpi::util::Color::WHITE); break; } } } TEST(LEDPatternTest, OffsetZero) { std::array buffer; auto offset = whiteYellowPurple.OffsetBy(0); offset.ApplyTo(buffer); for (int led = 0; led < 21; led++) { SCOPED_TRACE(fmt::format("LED {} of 21", led + 1)); switch (led % 3) { case 0: AssertIndexColor(buffer, led, wpi::util::Color::WHITE); break; case 1: AssertIndexColor(buffer, led, wpi::util::Color::YELLOW); break; case 2: AssertIndexColor(buffer, led, wpi::util::Color::PURPLE); break; } } } TEST(LEDPatternTest, BlinkSymmetric) { std::array buffer; auto white = LEDPattern::Solid(wpi::util::Color::WHITE); // on for 2 seconds, off for 2 seconds auto pattern = white.Blink(2_s); static uint64_t now = 0ull; WPI_SetNowImpl([] { return now; }); for (int t = 0; t < 8; t++) { now = t * 1000000ull; // time travel 1 second SCOPED_TRACE(fmt::format("Time {} seconds", t)); pattern.ApplyTo(buffer); switch (t) { case 0: case 1: case 4: case 5: AssertIndexColor(buffer, 0, wpi::util::Color::WHITE); break; case 2: case 3: case 6: case 7: AssertIndexColor(buffer, 0, wpi::util::Color::BLACK); break; } } WPI_SetNowImpl(nullptr); // cleanup } TEST(LEDPatternTest, BlinkAsymmetric) { std::array buffer; auto white = LEDPattern::Solid(wpi::util::Color::WHITE); // on for 3 seconds, off for 1 second auto pattern = white.Blink(3_s, 1_s); static uint64_t now = 0ull; WPI_SetNowImpl([] { return now; }); for (int t = 0; t < 8; t++) { now = t * 1000000ull; // time travel 1 second SCOPED_TRACE(fmt::format("Time {} seconds", t)); pattern.ApplyTo(buffer); switch (t) { case 0: case 1: case 2: // first period case 4: case 5: case 6: // second period AssertIndexColor(buffer, 0, wpi::util::Color::WHITE); break; case 3: case 7: AssertIndexColor(buffer, 0, wpi::util::Color::BLACK); break; } } WPI_SetNowImpl(nullptr); // cleanup } TEST(LEDPatternTest, BlinkInSync) { std::array buffer; auto white = LEDPattern::Solid(wpi::util::Color::WHITE); bool flag = false; auto condition = [&flag]() { return flag; }; auto pattern = white.SynchronizedBlink(condition); SCOPED_TRACE("Flag off"); pattern.ApplyTo(buffer); AssertIndexColor(buffer, 0, wpi::util::Color::BLACK); SCOPED_TRACE("Flag on"); flag = true; pattern.ApplyTo(buffer); AssertIndexColor(buffer, 0, wpi::util::Color::WHITE); SCOPED_TRACE("Flag off"); flag = false; pattern.ApplyTo(buffer); AssertIndexColor(buffer, 0, wpi::util::Color::BLACK); } TEST(LEDPatternTest, Breathe) { wpi::util::Color midGray{0.5, 0.5, 0.5}; std::array buffer; auto white = LEDPattern::Solid(wpi::util::Color::WHITE); auto pattern = white.Breathe(4_us); static uint64_t now = 0ull; WPI_SetNowImpl([] { return now; }); { now = 0ull; // start SCOPED_TRACE(fmt::format("Time {}", now)); pattern.ApplyTo(buffer); AssertIndexColor(buffer, 0, wpi::util::Color::WHITE); } { now = 1ull; // midway (down) SCOPED_TRACE(fmt::format("Time {}", now)); pattern.ApplyTo(buffer); AssertIndexColor(buffer, 0, midGray); } { now = 2ull; // bottom SCOPED_TRACE(fmt::format("Time {}", now)); pattern.ApplyTo(buffer); AssertIndexColor(buffer, 0, wpi::util::Color::BLACK); } { now = 3ull; // midway (up) SCOPED_TRACE(fmt::format("Time {}", now)); pattern.ApplyTo(buffer); AssertIndexColor(buffer, 0, midGray); } { now = 4ull; // back to start SCOPED_TRACE(fmt::format("Time {}", now)); pattern.ApplyTo(buffer); AssertIndexColor(buffer, 0, wpi::util::Color::WHITE); } WPI_SetNowImpl(nullptr); // cleanup } TEST(LEDPatternTest, OverlaySolidOnSolid) { std::array buffer; auto base = LEDPattern::Solid(wpi::util::Color::WHITE); auto overlay = LEDPattern::Solid(wpi::util::Color::YELLOW); auto pattern = overlay.OverlayOn(base); pattern.ApplyTo(buffer); AssertIndexColor(buffer, 0, wpi::util::Color::YELLOW); } TEST(LEDPatternTest, OverlayNearlyBlack) { std::array buffer; auto base = LEDPattern::Solid(wpi::util::Color::WHITE); auto overlay = LEDPattern::Solid(wpi::util::Color{1, 0, 0}); auto pattern = overlay.OverlayOn(base); pattern.ApplyTo(buffer); AssertIndexColor(buffer, 0, wpi::util::Color{1, 0, 0}); } TEST(LEDPatternTest, OverlayMixed) { std::array buffer; auto base = LEDPattern::Solid(wpi::util::Color::WHITE); std::array, 2> steps{ std::pair{0.0, wpi::util::Color::YELLOW}, std::pair{0.5, wpi::util::Color::BLACK}}; auto overlay = LEDPattern::Steps(steps); auto pattern = overlay.OverlayOn(base); pattern.ApplyTo(buffer); AssertIndexColor(buffer, 0, wpi::util::Color::YELLOW); AssertIndexColor(buffer, 1, wpi::util::Color::WHITE); } TEST(LEDPatternTest, Blend) { std::array buffer; auto pattern1 = LEDPattern::Solid(wpi::util::Color::BLUE); auto pattern2 = LEDPattern::Solid(wpi::util::Color::RED); auto blend = pattern1.Blend(pattern2); blend.ApplyTo(buffer); // Individual RGB channels are averaged // #0000FF blended with #FF0000 yields #7F007F AssertIndexColor(buffer, 0, wpi::util::Color{127, 0, 127}); } TEST(LEDPatternTest, BinaryMask) { std::array buffer; wpi::util::Color color{123, 123, 123}; auto base = LEDPattern::Solid(color); // first 50% mask on, last 50% mask off std::array, 2> steps{ std::pair{0.0, wpi::util::Color::WHITE}, std::pair{0.5, wpi::util::Color::BLACK}}; auto mask = LEDPattern::Steps(steps); auto masked = base.Mask(mask); masked.ApplyTo(buffer); for (int i = 0; i < 5; i++) { AssertIndexColor(buffer, i, color); } for (int i = 5; i < 10; i++) { AssertIndexColor(buffer, i, wpi::util::Color::BLACK); } } TEST(LEDPatternTest, ChannelwiseMask) { std::array buffer; wpi::util::Color baseColor{123, 123, 123}; wpi::util::Color halfGray{0.5, 0.5, 0.5}; auto base = LEDPattern::Solid(baseColor); std::array, 5> steps{ std::pair{0.0, wpi::util::Color::RED}, std::pair{0.2, wpi::util::Color::LIME}, std::pair{0.4, wpi::util::Color::BLUE}, std::pair{0.6, halfGray}, std::pair{0.8, wpi::util::Color::WHITE}}; auto mask = LEDPattern::Steps(steps); auto masked = base.Mask(mask); masked.ApplyTo(buffer); AssertIndexColor(buffer, 0, wpi::util::Color{123, 0, 0}); AssertIndexColor(buffer, 1, wpi::util::Color{0, 123, 0}); AssertIndexColor(buffer, 2, wpi::util::Color{0, 0, 123}); // mask channels are all 0b00111111, base is 0b00111011, // so the AND should give us the unmodified base color AssertIndexColor(buffer, 3, baseColor); AssertIndexColor(buffer, 4, baseColor); } TEST(LEDPatternTest, ProcessMaskLayer) { std::array buffer; double progress = 0.0; auto maskLayer = LEDPattern::ProgressMaskLayer([&progress]() { return progress; }); for (double t = 0; t <= 1.0; t += 0.01) { SCOPED_TRACE(fmt::format("Time {}", t)); progress = t; maskLayer.ApplyTo(buffer); int lastMaskedLED = static_cast(t * 100); for (int i = 0; i < lastMaskedLED; i++) { SCOPED_TRACE(fmt::format("LED {}", i)); AssertIndexColor(buffer, i, wpi::util::Color::WHITE); } for (int i = lastMaskedLED; i < 100; i++) { SCOPED_TRACE(fmt::format("LED {}", i)); AssertIndexColor(buffer, i, wpi::util::Color::BLACK); } } } TEST(LEDPatternTest, ZeroBrightness) { std::array buffer; auto base = LEDPattern::Solid(wpi::util::Color::RED); auto pattern = base.AtBrightness(0); pattern.ApplyTo(buffer); AssertIndexColor(buffer, 0, wpi::util::Color::BLACK); } TEST(LEDPatternTest, SameBrightness) { std::array buffer; auto base = LEDPattern::Solid(wpi::util::Color::MAGENTA); auto pattern = base.AtBrightness(1.0); pattern.ApplyTo(buffer); AssertIndexColor(buffer, 0, wpi::util::Color::MAGENTA); } TEST(LEDPatternTest, HigherBrightness) { std::array buffer; auto base = LEDPattern::Solid(wpi::util::Color::MAGENTA); auto pattern = base.AtBrightness(4 / 3.0); pattern.ApplyTo(buffer); AssertIndexColor(buffer, 0, wpi::util::Color::MAGENTA); } TEST(LEDPatternTest, NegativeBrightness) { std::array buffer; auto base = LEDPattern::Solid(wpi::util::Color::WHITE); auto pattern = base.AtBrightness(-1.0); pattern.ApplyTo(buffer); AssertIndexColor(buffer, 0, wpi::util::Color::BLACK); } TEST(LEDPatternTest, ClippingBrightness) { std::array buffer; auto base = LEDPattern::Solid(wpi::util::Color::MIDNIGHT_BLUE); auto pattern = base.AtBrightness(100); pattern.ApplyTo(buffer); AssertIndexColor(buffer, 0, wpi::util::Color::WHITE); } TEST(LEDPatternTest, ReverseMask) { std::array buffer; std::array, 4> colorSteps{ std::pair{0.0, wpi::util::Color::RED}, std::pair{0.25, wpi::util::Color::BLUE}, std::pair{0.5, wpi::util::Color::YELLOW}, std::pair{0.75, wpi::util::Color::GREEN}}; std::array, 2> maskSteps{ std::pair{0, wpi::util::Color::WHITE}, std::pair{0.5, wpi::util::Color::BLACK}}; auto pattern = LEDPattern::Steps(colorSteps) .Mask(LEDPattern::Steps(maskSteps)) .Reversed(); pattern.ApplyTo(buffer); AssertIndexColor(buffer, 7, wpi::util::Color::RED); AssertIndexColor(buffer, 6, wpi::util::Color::RED); AssertIndexColor(buffer, 5, wpi::util::Color::BLUE); AssertIndexColor(buffer, 4, wpi::util::Color::BLUE); AssertIndexColor(buffer, 3, wpi::util::Color::BLACK); AssertIndexColor(buffer, 2, wpi::util::Color::BLACK); AssertIndexColor(buffer, 1, wpi::util::Color::BLACK); AssertIndexColor(buffer, 0, wpi::util::Color::BLACK); } TEST(LEDPatternTest, OffsetMask) { std::array buffer; std::array, 4> colorSteps{ std::pair{0.0, wpi::util::Color::RED}, std::pair{0.25, wpi::util::Color::BLUE}, std::pair{0.5, wpi::util::Color::YELLOW}, std::pair{0.75, wpi::util::Color::GREEN}}; std::array, 2> maskSteps{ std::pair{0, wpi::util::Color::WHITE}, std::pair{0.5, wpi::util::Color::BLACK}}; auto pattern = LEDPattern::Steps(colorSteps) .Mask(LEDPattern::Steps(maskSteps)) .OffsetBy(4); pattern.ApplyTo(buffer); AssertIndexColor(buffer, 0, wpi::util::Color::BLACK); AssertIndexColor(buffer, 1, wpi::util::Color::BLACK); AssertIndexColor(buffer, 2, wpi::util::Color::BLACK); AssertIndexColor(buffer, 3, wpi::util::Color::BLACK); AssertIndexColor(buffer, 4, wpi::util::Color::RED); AssertIndexColor(buffer, 5, wpi::util::Color::RED); AssertIndexColor(buffer, 6, wpi::util::Color::BLUE); AssertIndexColor(buffer, 7, wpi::util::Color::BLUE); } TEST(LEDPatternTest, RelativeScrollingMask) { std::array buffer; std::array, 4> colorSteps{ std::pair{0.0, wpi::util::Color::RED}, std::pair{0.25, wpi::util::Color::BLUE}, std::pair{0.5, wpi::util::Color::YELLOW}, std::pair{0.75, wpi::util::Color::GREEN}}; std::array, 2> maskSteps{ std::pair{0, wpi::util::Color::WHITE}, std::pair{0.5, wpi::util::Color::BLACK}}; auto pattern = LEDPattern::Steps(colorSteps) .Mask(LEDPattern::Steps(maskSteps)) .ScrollAtRelativeVelocity(wpi::units::hertz_t{1e6 / 8.0}); pattern.ApplyTo(buffer); static uint64_t now = 0ull; WPI_SetNowImpl([] { return now; }); { now = 0ull; // start SCOPED_TRACE(fmt::format("Time {}", now)); pattern.ApplyTo(buffer); AssertIndexColor(buffer, 0, wpi::util::Color::RED); AssertIndexColor(buffer, 1, wpi::util::Color::RED); AssertIndexColor(buffer, 2, wpi::util::Color::BLUE); AssertIndexColor(buffer, 3, wpi::util::Color::BLUE); AssertIndexColor(buffer, 4, wpi::util::Color::BLACK); AssertIndexColor(buffer, 5, wpi::util::Color::BLACK); AssertIndexColor(buffer, 6, wpi::util::Color::BLACK); AssertIndexColor(buffer, 7, wpi::util::Color::BLACK); } { now = 1ull; SCOPED_TRACE(fmt::format("Time {}", now)); pattern.ApplyTo(buffer); AssertIndexColor(buffer, 0, wpi::util::Color::BLACK); AssertIndexColor(buffer, 1, wpi::util::Color::RED); AssertIndexColor(buffer, 2, wpi::util::Color::RED); AssertIndexColor(buffer, 3, wpi::util::Color::BLUE); AssertIndexColor(buffer, 4, wpi::util::Color::BLUE); AssertIndexColor(buffer, 5, wpi::util::Color::BLACK); AssertIndexColor(buffer, 6, wpi::util::Color::BLACK); AssertIndexColor(buffer, 7, wpi::util::Color::BLACK); } { now = 2ull; SCOPED_TRACE(fmt::format("Time {}", now)); pattern.ApplyTo(buffer); AssertIndexColor(buffer, 0, wpi::util::Color::BLACK); AssertIndexColor(buffer, 1, wpi::util::Color::BLACK); AssertIndexColor(buffer, 2, wpi::util::Color::RED); AssertIndexColor(buffer, 3, wpi::util::Color::RED); AssertIndexColor(buffer, 4, wpi::util::Color::BLUE); AssertIndexColor(buffer, 5, wpi::util::Color::BLUE); AssertIndexColor(buffer, 6, wpi::util::Color::BLACK); AssertIndexColor(buffer, 7, wpi::util::Color::BLACK); } { now = 3ull; SCOPED_TRACE(fmt::format("Time {}", now)); pattern.ApplyTo(buffer); AssertIndexColor(buffer, 0, wpi::util::Color::BLACK); AssertIndexColor(buffer, 1, wpi::util::Color::BLACK); AssertIndexColor(buffer, 2, wpi::util::Color::BLACK); AssertIndexColor(buffer, 3, wpi::util::Color::RED); AssertIndexColor(buffer, 4, wpi::util::Color::RED); AssertIndexColor(buffer, 5, wpi::util::Color::BLUE); AssertIndexColor(buffer, 6, wpi::util::Color::BLUE); AssertIndexColor(buffer, 7, wpi::util::Color::BLACK); } WPI_SetNowImpl(nullptr); // cleanup } TEST(LEDPatternTest, AbsoluteScrollingMask) { std::array buffer; std::array, 4> colorSteps{ std::pair{0.0, wpi::util::Color::RED}, std::pair{0.25, wpi::util::Color::BLUE}, std::pair{0.5, wpi::util::Color::YELLOW}, std::pair{0.75, wpi::util::Color::GREEN}}; std::array, 2> maskSteps{ std::pair{0, wpi::util::Color::WHITE}, std::pair{0.5, wpi::util::Color::BLACK}}; auto pattern = LEDPattern::Steps(colorSteps) .Mask(LEDPattern::Steps(maskSteps)) .ScrollAtAbsoluteVelocity(1_mps, 1_m); pattern.ApplyTo(buffer); static uint64_t now = 0ull; WPI_SetNowImpl([] { return now; }); { now = 0ull; // start SCOPED_TRACE(fmt::format("Time {}", now)); pattern.ApplyTo(buffer); AssertIndexColor(buffer, 0, wpi::util::Color::RED); AssertIndexColor(buffer, 1, wpi::util::Color::RED); AssertIndexColor(buffer, 2, wpi::util::Color::BLUE); AssertIndexColor(buffer, 3, wpi::util::Color::BLUE); AssertIndexColor(buffer, 4, wpi::util::Color::BLACK); AssertIndexColor(buffer, 5, wpi::util::Color::BLACK); AssertIndexColor(buffer, 6, wpi::util::Color::BLACK); AssertIndexColor(buffer, 7, wpi::util::Color::BLACK); } { now = 1000000ull; SCOPED_TRACE(fmt::format("Time {}", now)); pattern.ApplyTo(buffer); AssertIndexColor(buffer, 0, wpi::util::Color::BLACK); AssertIndexColor(buffer, 1, wpi::util::Color::RED); AssertIndexColor(buffer, 2, wpi::util::Color::RED); AssertIndexColor(buffer, 3, wpi::util::Color::BLUE); AssertIndexColor(buffer, 4, wpi::util::Color::BLUE); AssertIndexColor(buffer, 5, wpi::util::Color::BLACK); AssertIndexColor(buffer, 6, wpi::util::Color::BLACK); AssertIndexColor(buffer, 7, wpi::util::Color::BLACK); } { now = 2000000ull; SCOPED_TRACE(fmt::format("Time {}", now)); pattern.ApplyTo(buffer); AssertIndexColor(buffer, 0, wpi::util::Color::BLACK); AssertIndexColor(buffer, 1, wpi::util::Color::BLACK); AssertIndexColor(buffer, 2, wpi::util::Color::RED); AssertIndexColor(buffer, 3, wpi::util::Color::RED); AssertIndexColor(buffer, 4, wpi::util::Color::BLUE); AssertIndexColor(buffer, 5, wpi::util::Color::BLUE); AssertIndexColor(buffer, 6, wpi::util::Color::BLACK); AssertIndexColor(buffer, 7, wpi::util::Color::BLACK); } { now = 3000000ull; SCOPED_TRACE(fmt::format("Time {}", now)); pattern.ApplyTo(buffer); AssertIndexColor(buffer, 0, wpi::util::Color::BLACK); AssertIndexColor(buffer, 1, wpi::util::Color::BLACK); AssertIndexColor(buffer, 2, wpi::util::Color::BLACK); AssertIndexColor(buffer, 3, wpi::util::Color::RED); AssertIndexColor(buffer, 4, wpi::util::Color::RED); AssertIndexColor(buffer, 5, wpi::util::Color::BLUE); AssertIndexColor(buffer, 6, wpi::util::Color::BLUE); AssertIndexColor(buffer, 7, wpi::util::Color::BLACK); } WPI_SetNowImpl(nullptr); // cleanup } void AssertIndexColor(std::span data, int index, wpi::util::Color color) { wpi::util::Color8Bit color8bit{color}; EXPECT_EQ(color8bit.red, data[index].r & 0xFF); EXPECT_EQ(color8bit.green, data[index].g & 0xFF); EXPECT_EQ(color8bit.blue, data[index].b & 0xFF); } wpi::util::Color LerpColors(wpi::util::Color a, wpi::util::Color b, double t) { return wpi::util::Color{wpi::util::Lerp(a.red, b.red, t), wpi::util::Lerp(a.green, b.green, t), wpi::util::Lerp(a.blue, b.blue, t)}; } } // namespace wpi