[wpilibj, wpilibc] Fix LED patterns not offsetting reads (#6948)

Was causing bugs when combined with patterns that need to read back from the buffer (eg masks and overlays)

Co-authored-by: Joseph Eng <s-engjo@bsd405.org>
This commit is contained in:
Sam Carlberg
2024-11-29 00:25:54 -05:00
committed by GitHub
parent a0af0fd572
commit 5e1c6a84ce
5 changed files with 529 additions and 89 deletions

View File

@@ -18,33 +18,46 @@
using namespace frc;
LEDPattern::LEDPattern(LEDPatternFn impl) : m_impl(std::move(impl)) {}
LEDPattern::LEDPattern(std::function<void(frc::LEDPattern::LEDReader,
std::function<void(int, frc::Color)>)>
impl)
: m_impl(std::move(impl)) {}
void LEDPattern::ApplyTo(LEDPattern::LEDReader reader,
std::function<void(int, frc::Color)> writer) const {
m_impl(reader, writer);
}
void LEDPattern::ApplyTo(std::span<AddressableLED::LEDData> data,
LEDWriterFn writer) const {
m_impl(data, writer);
std::function<void(int, frc::Color)> writer) const {
ApplyTo(LEDPattern::LEDReader{[=](size_t i) { return data[i]; }, data.size()},
writer);
}
void LEDPattern::ApplyTo(std::span<AddressableLED::LEDData> data) const {
ApplyTo(data, [&](int index, Color color) { data[index].SetLED(color); });
}
LEDPattern LEDPattern::Reversed() {
return LEDPattern{[self = *this](auto data, auto writer) {
self.ApplyTo(data, [&](int i, Color color) {
writer((data.size() - 1) - i, color);
});
LEDPattern LEDPattern::MapIndex(
std::function<size_t(size_t, size_t)> indexMapper) {
return LEDPattern{[self = *this, indexMapper](auto data, auto writer) {
size_t bufLen = data.size();
self.ApplyTo(
LEDPattern::LEDReader{
[=](auto i) { return data[indexMapper(bufLen, i)]; }, bufLen},
[&](int i, Color color) { writer(indexMapper(bufLen, i), color); });
}};
}
LEDPattern LEDPattern::Reversed() {
return MapIndex([](size_t bufLen, size_t i) { return bufLen - 1 - i; });
}
LEDPattern LEDPattern::OffsetBy(int offset) {
return LEDPattern{[=, self = *this](auto data, auto writer) {
self.ApplyTo(data, [&data, &writer, offset](int i, Color color) {
int shiftedIndex =
frc::FloorMod(i + offset, static_cast<int>(data.size()));
writer(shiftedIndex, color);
});
}};
return MapIndex([offset](size_t bufLen, size_t i) {
return frc::FloorMod(static_cast<int>(i) + offset,
static_cast<int>(bufLen));
});
}
LEDPattern LEDPattern::ScrollAtRelativeSpeed(units::hertz_t velocity) {
@@ -53,8 +66,7 @@ LEDPattern LEDPattern::ScrollAtRelativeSpeed(units::hertz_t velocity) {
// Invert and multiply by 1,000,000 to get microseconds
double periodMicros = 1e6 / velocity.value();
return LEDPattern{[=, self = *this](auto data, auto writer) {
auto bufLen = data.size();
return MapIndex([=](size_t bufLen, size_t i) {
auto now = wpi::Now();
// index should move by (bufLen) / (period)
@@ -62,12 +74,9 @@ LEDPattern LEDPattern::ScrollAtRelativeSpeed(units::hertz_t velocity) {
(now % static_cast<int64_t>(std::floor(periodMicros))) / periodMicros;
int offset = static_cast<int>(std::floor(t * bufLen));
self.ApplyTo(data, [=](int i, Color color) {
// floorMod so if the offset is negative, we still get positive outputs
int shiftedIndex = frc::FloorMod(i + offset, static_cast<int>(bufLen));
writer(shiftedIndex, color);
});
}};
return frc::FloorMod(static_cast<int>(i) + offset,
static_cast<int>(bufLen));
});
}
LEDPattern LEDPattern::ScrollAtAbsoluteSpeed(
@@ -77,8 +86,7 @@ LEDPattern LEDPattern::ScrollAtAbsoluteSpeed(
auto microsPerLed =
static_cast<int64_t>(std::floor((ledSpacing / velocity).value() * 1e6));
return LEDPattern{[=, self = *this](auto data, auto writer) {
auto bufLen = data.size();
return MapIndex([=](size_t bufLen, size_t i) {
auto now = wpi::Now();
// every step in time that's a multiple of microsPerLED will increment
@@ -87,13 +95,9 @@ LEDPattern LEDPattern::ScrollAtAbsoluteSpeed(
// offset values for negative velocities
auto offset = static_cast<int64_t>(now) / microsPerLed;
self.ApplyTo(data, [=, &writer](int i, Color color) {
// FloorMod so if the offset is negative, we still get positive outputs
int shiftedIndex = frc::FloorMod(i + offset, static_cast<int>(bufLen));
writer(shiftedIndex, color);
});
}};
return frc::FloorMod(static_cast<int>(i) + offset,
static_cast<int>(bufLen));
});
}
LEDPattern LEDPattern::Blink(units::second_t onTime, units::second_t offTime) {

View File

@@ -18,21 +18,36 @@
namespace frc {
/**
* Sets the LED at the given index to the given color.
*/
using LEDWriterFn = std::function<void(int, frc::Color)>;
/**
* Accepts a data buffer (1st argument) and a callback (2nd argument) for
* writing data.
*/
using LEDPatternFn =
std::function<void(std::span<frc::AddressableLED::LEDData>, LEDWriterFn)>;
class LEDPattern {
public:
explicit LEDPattern(LEDPatternFn impl);
/**
* A wrapper around a length and an arbitrary reader function that accepts an
* LED index and returns data for the LED at that index. This configuration
* allows us to abstract over different container types without templating.
*/
class LEDReader {
public:
LEDReader(std::function<frc::AddressableLED::LEDData(int)> impl,
size_t size)
: m_impl{std::move(impl)}, m_size{size} {}
frc::AddressableLED::LEDData operator[](size_t index) const {
return m_impl(index);
}
size_t size() const { return m_size; }
private:
std::function<frc::AddressableLED::LEDData(int)> m_impl;
size_t m_size;
};
explicit LEDPattern(std::function<void(frc::LEDPattern::LEDReader,
std::function<void(int, frc::Color)>)>
impl);
void ApplyTo(LEDReader reader,
std::function<void(int, frc::Color)> writer) const;
/**
* Writes the pattern to an LED buffer. Dynamic animations should be called
@@ -48,7 +63,7 @@ class LEDPattern {
* @param writer data writer for setting new LED colors on the LED strip
*/
void ApplyTo(std::span<frc::AddressableLED::LEDData> data,
LEDWriterFn writer) const;
std::function<void(int, frc::Color)> writer) const;
/**
* Writes the pattern to an LED buffer. Dynamic animations should be called
@@ -64,6 +79,15 @@ class LEDPattern {
*/
void ApplyTo(std::span<frc::AddressableLED::LEDData> data) const;
/**
* Creates a pattern with remapped indices.
*
* @param indexMapper the index mapper
* @return the mapped pattern
*/
[[nodiscard]]
LEDPattern MapIndex(std::function<size_t(size_t, size_t)> indexMapper);
/**
* Creates a pattern that displays this one in reverse. Scrolling patterns
* will scroll in the opposite direction (but at the same speed). It will
@@ -373,6 +397,8 @@ class LEDPattern {
static LEDPattern Rainbow(int saturation, int value);
private:
LEDPatternFn m_impl;
std::function<void(frc::LEDPattern::LEDReader,
std::function<void(int, frc::Color)>)>
m_impl;
};
} // namespace frc

View File

@@ -813,6 +813,220 @@ TEST(LEDPatternTest, ClippingBrightness) {
AssertIndexColor(buffer, 0, Color::kWhite);
}
TEST(LEDPatternTest, ReverseMask) {
std::array<AddressableLED::LEDData, 8> buffer;
std::array<std::pair<double, Color>, 4> colorSteps{
std::pair{0.0, Color::kRed}, std::pair{0.25, Color::kBlue},
std::pair{0.5, Color::kYellow}, std::pair{0.75, Color::kGreen}};
std::array<std::pair<double, Color>, 2> maskSteps{
std::pair{0, Color::kWhite}, std::pair{0.5, Color::kBlack}};
auto pattern = LEDPattern::Steps(colorSteps)
.Mask(LEDPattern::Steps(maskSteps))
.Reversed();
pattern.ApplyTo(buffer);
AssertIndexColor(buffer, 7, Color::kRed);
AssertIndexColor(buffer, 6, Color::kRed);
AssertIndexColor(buffer, 5, Color::kBlue);
AssertIndexColor(buffer, 4, Color::kBlue);
AssertIndexColor(buffer, 3, Color::kBlack);
AssertIndexColor(buffer, 2, Color::kBlack);
AssertIndexColor(buffer, 1, Color::kBlack);
AssertIndexColor(buffer, 0, Color::kBlack);
}
TEST(LEDPatternTest, OffsetMask) {
std::array<AddressableLED::LEDData, 8> buffer;
std::array<std::pair<double, Color>, 4> colorSteps{
std::pair{0.0, Color::kRed}, std::pair{0.25, Color::kBlue},
std::pair{0.5, Color::kYellow}, std::pair{0.75, Color::kGreen}};
std::array<std::pair<double, Color>, 2> maskSteps{
std::pair{0, Color::kWhite}, std::pair{0.5, Color::kBlack}};
auto pattern = LEDPattern::Steps(colorSteps)
.Mask(LEDPattern::Steps(maskSteps))
.OffsetBy(4);
pattern.ApplyTo(buffer);
AssertIndexColor(buffer, 0, Color::kBlack);
AssertIndexColor(buffer, 1, Color::kBlack);
AssertIndexColor(buffer, 2, Color::kBlack);
AssertIndexColor(buffer, 3, Color::kBlack);
AssertIndexColor(buffer, 4, Color::kRed);
AssertIndexColor(buffer, 5, Color::kRed);
AssertIndexColor(buffer, 6, Color::kBlue);
AssertIndexColor(buffer, 7, Color::kBlue);
}
TEST(LEDPatternTest, RelativeScrollingMask) {
std::array<AddressableLED::LEDData, 8> buffer;
std::array<std::pair<double, Color>, 4> colorSteps{
std::pair{0.0, Color::kRed}, std::pair{0.25, Color::kBlue},
std::pair{0.5, Color::kYellow}, std::pair{0.75, Color::kGreen}};
std::array<std::pair<double, Color>, 2> maskSteps{
std::pair{0, Color::kWhite}, std::pair{0.5, Color::kBlack}};
auto pattern = LEDPattern::Steps(colorSteps)
.Mask(LEDPattern::Steps(maskSteps))
.ScrollAtRelativeSpeed(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, Color::kRed);
AssertIndexColor(buffer, 1, Color::kRed);
AssertIndexColor(buffer, 2, Color::kBlue);
AssertIndexColor(buffer, 3, Color::kBlue);
AssertIndexColor(buffer, 4, Color::kBlack);
AssertIndexColor(buffer, 5, Color::kBlack);
AssertIndexColor(buffer, 6, Color::kBlack);
AssertIndexColor(buffer, 7, Color::kBlack);
}
{
now = 1ull;
SCOPED_TRACE(fmt::format("Time {}", now));
pattern.ApplyTo(buffer);
AssertIndexColor(buffer, 0, Color::kBlack);
AssertIndexColor(buffer, 1, Color::kRed);
AssertIndexColor(buffer, 2, Color::kRed);
AssertIndexColor(buffer, 3, Color::kBlue);
AssertIndexColor(buffer, 4, Color::kBlue);
AssertIndexColor(buffer, 5, Color::kBlack);
AssertIndexColor(buffer, 6, Color::kBlack);
AssertIndexColor(buffer, 7, Color::kBlack);
}
{
now = 2ull;
SCOPED_TRACE(fmt::format("Time {}", now));
pattern.ApplyTo(buffer);
AssertIndexColor(buffer, 0, Color::kBlack);
AssertIndexColor(buffer, 1, Color::kBlack);
AssertIndexColor(buffer, 2, Color::kRed);
AssertIndexColor(buffer, 3, Color::kRed);
AssertIndexColor(buffer, 4, Color::kBlue);
AssertIndexColor(buffer, 5, Color::kBlue);
AssertIndexColor(buffer, 6, Color::kBlack);
AssertIndexColor(buffer, 7, Color::kBlack);
}
{
now = 3ull;
SCOPED_TRACE(fmt::format("Time {}", now));
pattern.ApplyTo(buffer);
AssertIndexColor(buffer, 0, Color::kBlack);
AssertIndexColor(buffer, 1, Color::kBlack);
AssertIndexColor(buffer, 2, Color::kBlack);
AssertIndexColor(buffer, 3, Color::kRed);
AssertIndexColor(buffer, 4, Color::kRed);
AssertIndexColor(buffer, 5, Color::kBlue);
AssertIndexColor(buffer, 6, Color::kBlue);
AssertIndexColor(buffer, 7, Color::kBlack);
}
WPI_SetNowImpl(nullptr); // cleanup
}
TEST(LEDPatternTest, AbsoluteScrollingMask) {
std::array<AddressableLED::LEDData, 8> buffer;
std::array<std::pair<double, Color>, 4> colorSteps{
std::pair{0.0, Color::kRed}, std::pair{0.25, Color::kBlue},
std::pair{0.5, Color::kYellow}, std::pair{0.75, Color::kGreen}};
std::array<std::pair<double, Color>, 2> maskSteps{
std::pair{0, Color::kWhite}, std::pair{0.5, Color::kBlack}};
auto pattern = LEDPattern::Steps(colorSteps)
.Mask(LEDPattern::Steps(maskSteps))
.ScrollAtAbsoluteSpeed(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, Color::kRed);
AssertIndexColor(buffer, 1, Color::kRed);
AssertIndexColor(buffer, 2, Color::kBlue);
AssertIndexColor(buffer, 3, Color::kBlue);
AssertIndexColor(buffer, 4, Color::kBlack);
AssertIndexColor(buffer, 5, Color::kBlack);
AssertIndexColor(buffer, 6, Color::kBlack);
AssertIndexColor(buffer, 7, Color::kBlack);
}
{
now = 1000000ull;
SCOPED_TRACE(fmt::format("Time {}", now));
pattern.ApplyTo(buffer);
AssertIndexColor(buffer, 0, Color::kBlack);
AssertIndexColor(buffer, 1, Color::kRed);
AssertIndexColor(buffer, 2, Color::kRed);
AssertIndexColor(buffer, 3, Color::kBlue);
AssertIndexColor(buffer, 4, Color::kBlue);
AssertIndexColor(buffer, 5, Color::kBlack);
AssertIndexColor(buffer, 6, Color::kBlack);
AssertIndexColor(buffer, 7, Color::kBlack);
}
{
now = 2000000ull;
SCOPED_TRACE(fmt::format("Time {}", now));
pattern.ApplyTo(buffer);
AssertIndexColor(buffer, 0, Color::kBlack);
AssertIndexColor(buffer, 1, Color::kBlack);
AssertIndexColor(buffer, 2, Color::kRed);
AssertIndexColor(buffer, 3, Color::kRed);
AssertIndexColor(buffer, 4, Color::kBlue);
AssertIndexColor(buffer, 5, Color::kBlue);
AssertIndexColor(buffer, 6, Color::kBlack);
AssertIndexColor(buffer, 7, Color::kBlack);
}
{
now = 3000000ull;
SCOPED_TRACE(fmt::format("Time {}", now));
pattern.ApplyTo(buffer);
AssertIndexColor(buffer, 0, Color::kBlack);
AssertIndexColor(buffer, 1, Color::kBlack);
AssertIndexColor(buffer, 2, Color::kBlack);
AssertIndexColor(buffer, 3, Color::kRed);
AssertIndexColor(buffer, 4, Color::kRed);
AssertIndexColor(buffer, 5, Color::kBlue);
AssertIndexColor(buffer, 6, Color::kBlue);
AssertIndexColor(buffer, 7, Color::kBlack);
}
WPI_SetNowImpl(nullptr); // cleanup
}
void AssertIndexColor(std::span<AddressableLED::LEDData> data, int index,
Color color) {
frc::Color8Bit color8bit{color};

View File

@@ -93,6 +93,19 @@ import java.util.function.DoubleSupplier;
*/
@FunctionalInterface
public interface LEDPattern {
/** A functional interface for index mapping functions. */
@FunctionalInterface
interface IndexMapper {
/**
* Maps the index.
*
* @param bufLen Length of the buffer
* @param index The index to map
* @return The mapped index
*/
int apply(int bufLen, int index);
}
/**
* Writes the pattern to an LED buffer. Dynamic animations should be called periodically (such as
* with a command or with a periodic method) to refresh the buffer over time.
@@ -129,6 +142,41 @@ public interface LEDPattern {
applyTo(readWriter, readWriter);
}
/**
* Creates a pattern with remapped indices.
*
* @param indexMapper the index mapper
* @return the mapped pattern
*/
default LEDPattern mapIndex(IndexMapper indexMapper) {
return (reader, writer) -> {
int bufLen = reader.getLength();
applyTo(
new LEDReader() {
@Override
public int getLength() {
return reader.getLength();
}
@Override
public int getRed(int index) {
return reader.getRed(indexMapper.apply(bufLen, index));
}
@Override
public int getGreen(int index) {
return reader.getGreen(indexMapper.apply(bufLen, index));
}
@Override
public int getBlue(int index) {
return reader.getBlue(indexMapper.apply(bufLen, index));
}
},
(i, r, g, b) -> writer.setRGB(indexMapper.apply(bufLen, i), r, g, b));
};
}
/**
* Creates a pattern that displays this one in reverse. Scrolling patterns will scroll in the
* opposite direction (but at the same speed). It will treat the end of an LED strip as the start,
@@ -143,10 +191,7 @@ public interface LEDPattern {
* @see AddressableLEDBufferView#reversed()
*/
default LEDPattern reversed() {
return (reader, writer) -> {
int bufLen = reader.getLength();
applyTo(reader, (i, r, g, b) -> writer.setRGB((bufLen - 1) - i, r, g, b));
};
return mapIndex((length, index) -> length - 1 - index);
}
/**
@@ -157,15 +202,7 @@ public interface LEDPattern {
* @return the offset pattern
*/
default LEDPattern offsetBy(int offset) {
return (reader, writer) -> {
int bufLen = reader.getLength();
applyTo(
reader,
(i, r, g, b) -> {
int shiftedIndex = Math.floorMod(i + offset, bufLen);
writer.setRGB(shiftedIndex, r, g, b);
});
};
return mapIndex((length, index) -> Math.floorMod(index + offset, length));
}
/**
@@ -189,23 +226,16 @@ public interface LEDPattern {
default LEDPattern scrollAtRelativeSpeed(Frequency velocity) {
final double periodMicros = velocity.asPeriod().in(Microseconds);
return (reader, writer) -> {
int bufLen = reader.getLength();
long now = RobotController.getTime();
return mapIndex(
(bufLen, index) -> {
long now = RobotController.getTime();
// index should move by (buf.length) / (period)
double t = (now % (long) periodMicros) / periodMicros;
int offset = (int) (t * bufLen);
// index should move by (buf.length) / (period)
double t = (now % (long) periodMicros) / periodMicros;
int offset = (int) (t * bufLen);
applyTo(
reader,
(i, r, g, b) -> {
// floorMod so if the offset is negative, we still get positive outputs
int shiftedIndex = Math.floorMod(i + offset, bufLen);
writer.setRGB(shiftedIndex, r, g, b);
});
};
return Math.floorMod(index + offset, bufLen);
});
}
/**
@@ -240,22 +270,16 @@ public interface LEDPattern {
var metersPerMicro = velocity.in(Meters.per(Microsecond));
var microsPerLED = (int) (ledSpacing.in(Meters) / metersPerMicro);
return (reader, writer) -> {
int bufLen = reader.getLength();
long now = RobotController.getTime();
return mapIndex(
(bufLen, index) -> {
long now = RobotController.getTime();
// every step in time that's a multiple of microsPerLED will increment the offset by 1
var offset = now / microsPerLED;
// every step in time that's a multiple of microsPerLED will increment the offset by 1
var offset = (int) (now / microsPerLED);
applyTo(
reader,
(i, r, g, b) -> {
// floorMod so if the offset is negative, we still get positive outputs
int shiftedIndex = Math.floorMod(i + offset, bufLen);
writer.setRGB(shiftedIndex, r, g, b);
});
};
// floorMod so if the offset is negative, we still get positive outputs
return Math.floorMod(index + offset, bufLen);
});
}
/**

View File

@@ -5,6 +5,7 @@
package edu.wpi.first.wpilibj;
import static edu.wpi.first.units.Units.Centimeters;
import static edu.wpi.first.units.Units.Meters;
import static edu.wpi.first.units.Units.MetersPerSecond;
import static edu.wpi.first.units.Units.Microsecond;
import static edu.wpi.first.units.Units.Microseconds;
@@ -15,6 +16,7 @@ import static edu.wpi.first.wpilibj.LEDPattern.GradientType.kContinuous;
import static edu.wpi.first.wpilibj.LEDPattern.GradientType.kDiscontinuous;
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.kGreen;
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;
@@ -815,6 +817,176 @@ class LEDPatternTest {
assertColorEquals(kWhite, buffer.getLED(0));
}
@Test
void reverseMask() {
var pattern =
LEDPattern.steps(Map.of(0, kRed, 0.25, kBlue, 0.5, kYellow, 0.75, kGreen))
.mask(LEDPattern.steps(Map.of(0, kWhite, 0.5, kBlack)))
.reversed();
var buffer = new AddressableLEDBuffer(8);
pattern.applyTo(buffer);
assertColorEquals(kRed, buffer.getLED(7));
assertColorEquals(kRed, buffer.getLED(6));
assertColorEquals(kBlue, buffer.getLED(5));
assertColorEquals(kBlue, buffer.getLED(4));
assertColorEquals(kBlack, buffer.getLED(3));
assertColorEquals(kBlack, buffer.getLED(2));
assertColorEquals(kBlack, buffer.getLED(1));
assertColorEquals(kBlack, buffer.getLED(0));
}
@Test
void offsetMask() {
var pattern =
LEDPattern.steps(Map.of(0, kRed, 0.25, kBlue, 0.5, kYellow, 0.75, kGreen))
.mask(LEDPattern.steps(Map.of(0, kWhite, 0.5, kBlack)))
.offsetBy(4);
var buffer = new AddressableLEDBuffer(8);
pattern.applyTo(buffer);
assertColorEquals(kBlack, buffer.getLED(0));
assertColorEquals(kBlack, buffer.getLED(1));
assertColorEquals(kBlack, buffer.getLED(2));
assertColorEquals(kBlack, buffer.getLED(3));
assertColorEquals(kRed, buffer.getLED(4));
assertColorEquals(kRed, buffer.getLED(5));
assertColorEquals(kBlue, buffer.getLED(6));
assertColorEquals(kBlue, buffer.getLED(7));
}
@Test
void relativeScrollingMask() {
// [red, red, blue, blue, yellow, yellow, green, green]
// under a mask of first 50% on, last 50% off
// [red, red, blue, blue, black, black, black, black]
// all scrolling at 1 LED per microsecond
var pattern =
LEDPattern.steps(Map.of(0, kRed, 0.25, kBlue, 0.5, kYellow, 0.75, kGreen))
.mask(LEDPattern.steps(Map.of(0, kWhite, 0.5, kBlack)))
.scrollAtRelativeSpeed(Percent.per(Microsecond).of(12.5));
var buffer = new AddressableLEDBuffer(8);
{
m_mockTime = 0; // start
pattern.applyTo(buffer);
assertColorEquals(kRed, buffer.getLED(0));
assertColorEquals(kRed, buffer.getLED(1));
assertColorEquals(kBlue, buffer.getLED(2));
assertColorEquals(kBlue, buffer.getLED(3));
assertColorEquals(kBlack, buffer.getLED(4));
assertColorEquals(kBlack, buffer.getLED(5));
assertColorEquals(kBlack, buffer.getLED(6));
assertColorEquals(kBlack, buffer.getLED(7));
}
{
m_mockTime = 1;
pattern.applyTo(buffer);
assertColorEquals(kBlack, buffer.getLED(0));
assertColorEquals(kRed, buffer.getLED(1));
assertColorEquals(kRed, buffer.getLED(2));
assertColorEquals(kBlue, buffer.getLED(3));
assertColorEquals(kBlue, buffer.getLED(4));
assertColorEquals(kBlack, buffer.getLED(5));
assertColorEquals(kBlack, buffer.getLED(6));
assertColorEquals(kBlack, buffer.getLED(7));
}
{
m_mockTime = 2;
pattern.applyTo(buffer);
assertColorEquals(kBlack, buffer.getLED(0));
assertColorEquals(kBlack, buffer.getLED(1));
assertColorEquals(kRed, buffer.getLED(2));
assertColorEquals(kRed, buffer.getLED(3));
assertColorEquals(kBlue, buffer.getLED(4));
assertColorEquals(kBlue, buffer.getLED(5));
assertColorEquals(kBlack, buffer.getLED(6));
assertColorEquals(kBlack, buffer.getLED(7));
}
{
m_mockTime = 3;
pattern.applyTo(buffer);
assertColorEquals(kBlack, buffer.getLED(0));
assertColorEquals(kBlack, buffer.getLED(1));
assertColorEquals(kBlack, buffer.getLED(2));
assertColorEquals(kRed, buffer.getLED(3));
assertColorEquals(kRed, buffer.getLED(4));
assertColorEquals(kBlue, buffer.getLED(5));
assertColorEquals(kBlue, buffer.getLED(6));
assertColorEquals(kBlack, buffer.getLED(7));
}
}
@Test
void absoluteScrollingMask() {
// [red, red, blue, blue, yellow, yellow, green, green]
// under a mask of first 50% on, last 50% off
// [red, red, blue, blue, black, black, black, black]
// all scrolling at 1 LED per microsecond
var pattern =
LEDPattern.steps(Map.of(0, kRed, 0.25, kBlue, 0.5, kYellow, 0.75, kGreen))
.mask(LEDPattern.steps(Map.of(0, kWhite, 0.5, kBlack)))
.scrollAtAbsoluteSpeed(Meters.per(Microsecond).of(1), Meters.one());
var buffer = new AddressableLEDBuffer(8);
{
m_mockTime = 0; // start
pattern.applyTo(buffer);
assertColorEquals(kRed, buffer.getLED(0));
assertColorEquals(kRed, buffer.getLED(1));
assertColorEquals(kBlue, buffer.getLED(2));
assertColorEquals(kBlue, buffer.getLED(3));
assertColorEquals(kBlack, buffer.getLED(4));
assertColorEquals(kBlack, buffer.getLED(5));
assertColorEquals(kBlack, buffer.getLED(6));
assertColorEquals(kBlack, buffer.getLED(7));
}
{
m_mockTime = 1;
pattern.applyTo(buffer);
assertColorEquals(kBlack, buffer.getLED(0));
assertColorEquals(kRed, buffer.getLED(1));
assertColorEquals(kRed, buffer.getLED(2));
assertColorEquals(kBlue, buffer.getLED(3));
assertColorEquals(kBlue, buffer.getLED(4));
assertColorEquals(kBlack, buffer.getLED(5));
assertColorEquals(kBlack, buffer.getLED(6));
assertColorEquals(kBlack, buffer.getLED(7));
}
{
m_mockTime = 2;
pattern.applyTo(buffer);
assertColorEquals(kBlack, buffer.getLED(0));
assertColorEquals(kBlack, buffer.getLED(1));
assertColorEquals(kRed, buffer.getLED(2));
assertColorEquals(kRed, buffer.getLED(3));
assertColorEquals(kBlue, buffer.getLED(4));
assertColorEquals(kBlue, buffer.getLED(5));
assertColorEquals(kBlack, buffer.getLED(6));
assertColorEquals(kBlack, buffer.getLED(7));
}
{
m_mockTime = 3;
pattern.applyTo(buffer);
assertColorEquals(kBlack, buffer.getLED(0));
assertColorEquals(kBlack, buffer.getLED(1));
assertColorEquals(kBlack, buffer.getLED(2));
assertColorEquals(kRed, buffer.getLED(3));
assertColorEquals(kRed, buffer.getLED(4));
assertColorEquals(kBlue, buffer.getLED(5));
assertColorEquals(kBlue, buffer.getLED(6));
assertColorEquals(kBlack, buffer.getLED(7));
}
}
void assertColorEquals(Color expected, Color actual) {
assertEquals(new Color8Bit(expected), new Color8Bit(actual));
}