001// Copyright (c) FIRST and other WPILib contributors. 002// Open Source Software; you can modify and/or share it under the terms of 003// the WPILib BSD license file in the root directory of this project. 004 005package edu.wpi.first.wpilibj; 006 007import static edu.wpi.first.units.Units.Meters; 008import static edu.wpi.first.units.Units.Microsecond; 009import static edu.wpi.first.units.Units.Microseconds; 010import static edu.wpi.first.units.Units.Value; 011 012import edu.wpi.first.hal.FRCNetComm.tResourceType; 013import edu.wpi.first.hal.HAL; 014import edu.wpi.first.math.MathUtil; 015import edu.wpi.first.units.collections.LongToObjectHashMap; 016import edu.wpi.first.units.measure.Dimensionless; 017import edu.wpi.first.units.measure.Distance; 018import edu.wpi.first.units.measure.Frequency; 019import edu.wpi.first.units.measure.LinearVelocity; 020import edu.wpi.first.units.measure.Time; 021import edu.wpi.first.util.WPIUtilJNI; 022import edu.wpi.first.wpilibj.util.Color; 023import java.util.Map; 024import java.util.Objects; 025import java.util.function.BooleanSupplier; 026import java.util.function.DoubleSupplier; 027 028/** 029 * An LED pattern controls lights on an LED strip to command patterns of color that may change over 030 * time. Dynamic patterns should synchronize on an external clock for timed-based animations ({@link 031 * WPIUtilJNI#now()} is recommended, since it can be mocked in simulation and unit tests), or on 032 * some other dynamic input (see {@link #synchronizedBlink(BooleanSupplier)}, for example). 033 * 034 * <p>Patterns should be updated periodically in order for animations to play smoothly. For example, 035 * a hypothetical LED subsystem could create a {@code Command} that will continuously apply the 036 * pattern to its LED data buffer as part of the main periodic loop. 037 * 038 * <pre><code> 039 * public class LEDs extends SubsystemBase { 040 * private final AddressableLED m_led = new AddressableLED(0); 041 * private final AddressableLEDBuffer m_ledData = new AddressableLEDBuffer(120); 042 * 043 * public LEDs() { 044 * m_led.setLength(120); 045 * m_led.start(); 046 * } 047 * 048 * {@literal @}Override 049 * public void periodic() { 050 * m_led.writeData(m_ledData); 051 * } 052 * 053 * public Command runPattern(LEDPattern pattern) { 054 * return run(() -> pattern.applyTo(m_ledData)); 055 * } 056 * } 057 * </code></pre> 058 * 059 * <p>LED patterns are stateless, and as such can be applied to multiple LED strips (or different 060 * sections of the same LED strip, since the roboRIO can only drive a single LED strip). In this 061 * example, we split the single buffer into two views - one for the section of the LED strip on the 062 * left side of a robot, and another view for the section of LEDs on the right side. The same 063 * pattern is able to be applied to both sides. 064 * 065 * <pre><code> 066 * public class LEDs extends SubsystemBase { 067 * private final AddressableLED m_led = new AddressableLED(0); 068 * private final AddressableLEDBuffer m_ledData = new AddressableLEDBuffer(60); 069 * private final AddressableLEDBufferView m_leftData = m_ledData.createView(0, 29); 070 * private final AddressableLEDBufferView m_rightData = m_ledData.createView(30, 59).reversed(); 071 * 072 * public LEDs() { 073 * m_led.setLength(60); 074 * m_led.start(); 075 * } 076 * 077 * {@literal @}Override 078 * public void periodic() { 079 * m_led.writeData(m_ledData); 080 * } 081 * 082 * public Command runPattern(LEDPattern pattern) { 083 * // Use the single input pattern to drive both sides 084 * return runSplitPatterns(pattern, pattern); 085 * } 086 * 087 * public Command runSplitPatterns(LEDPattern left, LEDPattern right) { 088 * return run(() -> { 089 * left.applyTo(m_leftData); 090 * right.applyTo(m_rightData); 091 * }); 092 * } 093 * } 094 * </code></pre> 095 */ 096@FunctionalInterface 097public interface LEDPattern { 098 /** A functional interface for index mapping functions. */ 099 @FunctionalInterface 100 interface IndexMapper { 101 /** 102 * Maps the index. 103 * 104 * @param bufLen Length of the buffer 105 * @param index The index to map 106 * @return The mapped index 107 */ 108 int apply(int bufLen, int index); 109 } 110 111 /** 112 * Writes the pattern to an LED buffer. Dynamic animations should be called periodically (such as 113 * with a command or with a periodic method) to refresh the buffer over time. 114 * 115 * <p>This method is intentionally designed to use separate objects for reading and writing data. 116 * By splitting them up, we can easily modify the behavior of some base pattern to make it {@link 117 * #scrollAtRelativeSpeed(Frequency) scroll}, {@link #blink(Time, Time) blink}, or {@link 118 * #breathe(Time) breathe} by intercepting the data writes to transform their behavior to whatever 119 * we like. 120 * 121 * @param reader data reader for accessing buffer length and current colors 122 * @param writer data writer for setting new LED colors on the buffer 123 */ 124 void applyTo(LEDReader reader, LEDWriter writer); 125 126 /** 127 * Convenience for {@link #applyTo(LEDReader, LEDWriter)} when one object provides both a read and 128 * a write interface. This is most helpful for playing an animated pattern directly on an {@link 129 * AddressableLEDBuffer} for the sake of code clarity. 130 * 131 * <pre><code> 132 * AddressableLEDBuffer data = new AddressableLEDBuffer(120); 133 * LEDPattern pattern = ... 134 * 135 * void periodic() { 136 * pattern.applyTo(data); 137 * } 138 * </code></pre> 139 * 140 * @param readWriter the object to use for both reading and writing to a set of LEDs 141 * @param <T> the type of the object that can both read and write LED data 142 */ 143 default <T extends LEDReader & LEDWriter> void applyTo(T readWriter) { 144 applyTo(readWriter, readWriter); 145 } 146 147 /** 148 * Creates a pattern with remapped indices. 149 * 150 * @param indexMapper the index mapper 151 * @return the mapped pattern 152 */ 153 default LEDPattern mapIndex(IndexMapper indexMapper) { 154 HAL.report(tResourceType.kResourceType_LEDPattern, 1); 155 return (reader, writer) -> { 156 int bufLen = reader.getLength(); 157 applyTo( 158 new LEDReader() { 159 @Override 160 public int getLength() { 161 return reader.getLength(); 162 } 163 164 @Override 165 public int getRed(int index) { 166 return reader.getRed(indexMapper.apply(bufLen, index)); 167 } 168 169 @Override 170 public int getGreen(int index) { 171 return reader.getGreen(indexMapper.apply(bufLen, index)); 172 } 173 174 @Override 175 public int getBlue(int index) { 176 return reader.getBlue(indexMapper.apply(bufLen, index)); 177 } 178 }, 179 (i, r, g, b) -> writer.setRGB(indexMapper.apply(bufLen, i), r, g, b)); 180 }; 181 } 182 183 /** 184 * Creates a pattern that displays this one in reverse. Scrolling patterns will scroll in the 185 * opposite direction (but at the same speed). It will treat the end of an LED strip as the start, 186 * and the start of the strip as the end. This can be useful for making ping-pong patterns that 187 * travel from one end of an LED strip to the other, then reverse direction and move back to the 188 * start. This can also be useful when working with LED strips connected in a serpentine pattern 189 * (where the start of one strip is connected to the end of the previous one); however, consider 190 * using a {@link AddressableLEDBufferView#reversed() reversed view} of the overall buffer for 191 * that segment rather than reversing patterns. 192 * 193 * @return the reverse pattern 194 * @see AddressableLEDBufferView#reversed() 195 */ 196 default LEDPattern reversed() { 197 return mapIndex((length, index) -> length - 1 - index); 198 } 199 200 /** 201 * Creates a pattern that plays this one, but offset by a certain number of LEDs. The offset 202 * pattern will wrap around, if necessary. 203 * 204 * @param offset how many LEDs to offset by 205 * @return the offset pattern 206 */ 207 default LEDPattern offsetBy(int offset) { 208 return mapIndex((length, index) -> Math.floorMod(index + offset, length)); 209 } 210 211 /** 212 * Creates a pattern that plays this one scrolling up the buffer. The velocity controls how fast 213 * the pattern returns back to its original position, and is in terms of the length of the LED 214 * strip; scrolling across a segment that is 10 LEDs long will travel twice as fast as on a 215 * segment that's only 5 LEDs long (assuming equal LED density on both segments). 216 * 217 * <p>For example, scrolling a pattern by one quarter of any LED strip's length per second, 218 * regardless of the total number of LEDs on that strip: 219 * 220 * <pre> 221 * LEDPattern rainbow = LEDPattern.rainbow(255, 255); 222 * LEDPattern scrollingRainbow = rainbow.scrollAtRelativeSpeed(Percent.per(Second).of(25)); 223 * </pre> 224 * 225 * @param velocity how fast the pattern should move, in terms of how long it takes to do a full 226 * scroll along the length of LEDs and return back to the starting position 227 * @return the scrolling pattern 228 */ 229 default LEDPattern scrollAtRelativeSpeed(Frequency velocity) { 230 final double periodMicros = velocity.asPeriod().in(Microseconds); 231 232 return mapIndex( 233 (bufLen, index) -> { 234 long now = RobotController.getTime(); 235 236 // index should move by (buf.length) / (period) 237 double t = (now % (long) periodMicros) / periodMicros; 238 int offset = (int) (t * bufLen); 239 240 return Math.floorMod(index + offset, bufLen); 241 }); 242 } 243 244 /** 245 * Creates a pattern that plays this one scrolling up an LED strip. A negative velocity makes the 246 * pattern play in reverse. 247 * 248 * <p>For example, scrolling a pattern at 4 inches per second along an LED strip with 60 LEDs per 249 * meter: 250 * 251 * <pre> 252 * // LEDs per meter, a known value taken from the spec sheet of our particular LED strip 253 * Distance LED_SPACING = Meters.of(1.0 / 60); 254 * 255 * LEDPattern rainbow = LEDPattern.rainbow(); 256 * LEDPattern scrollingRainbow = 257 * rainbow.scrollAtAbsoluteSpeed(InchesPerSecond.of(4), LED_SPACING); 258 * </pre> 259 * 260 * <p>Note that this pattern will scroll <i>faster</i> if applied to a less dense LED strip (such 261 * as 30 LEDs per meter), or <i>slower</i> if applied to a denser LED strip (such as 120 or 144 262 * LEDs per meter). 263 * 264 * @param velocity how fast the pattern should move along a physical LED strip 265 * @param ledSpacing the distance between adjacent LEDs on the physical LED strip 266 * @return the scrolling pattern 267 */ 268 default LEDPattern scrollAtAbsoluteSpeed(LinearVelocity velocity, Distance ledSpacing) { 269 // eg velocity = 10 m/s, spacing = 0.01m 270 // meters per micro = 1e-5 m/us 271 // micros per LED = 1e-2 m / (1e-5 m/us) = 1e-3 us 272 273 var metersPerMicro = velocity.in(Meters.per(Microsecond)); 274 var microsPerLED = (int) (ledSpacing.in(Meters) / metersPerMicro); 275 276 return mapIndex( 277 (bufLen, index) -> { 278 long now = RobotController.getTime(); 279 280 // every step in time that's a multiple of microsPerLED will increment the offset by 1 281 var offset = (int) (now / microsPerLED); 282 283 // floorMod so if the offset is negative, we still get positive outputs 284 return Math.floorMod(index + offset, bufLen); 285 }); 286 } 287 288 /** 289 * Creates a pattern that switches between playing this pattern and turning the entire LED strip 290 * off. 291 * 292 * @param onTime how long the pattern should play for, per cycle 293 * @param offTime how long the pattern should be turned off for, per cycle 294 * @return the blinking pattern 295 */ 296 default LEDPattern blink(Time onTime, Time offTime) { 297 final long totalTimeMicros = (long) (onTime.in(Microseconds) + offTime.in(Microseconds)); 298 final long onTimeMicros = (long) onTime.in(Microseconds); 299 300 HAL.report(tResourceType.kResourceType_LEDPattern, 1); 301 return (reader, writer) -> { 302 if (RobotController.getTime() % totalTimeMicros < onTimeMicros) { 303 applyTo(reader, writer); 304 } else { 305 kOff.applyTo(reader, writer); 306 } 307 }; 308 } 309 310 /** 311 * Like {@link #blink(Time, Time) blink(onTime, offTime)}, but where the "off" time is exactly 312 * equal to the "on" time. 313 * 314 * @param onTime how long the pattern should play for (and be turned off for), per cycle 315 * @return the blinking pattern 316 */ 317 default LEDPattern blink(Time onTime) { 318 return blink(onTime, onTime); 319 } 320 321 /** 322 * Creates a pattern that blinks this one on and off in sync with a true/false signal. The pattern 323 * will play while the signal outputs {@code true}, and will turn off while the signal outputs 324 * {@code false}. 325 * 326 * @param signal the signal to synchronize with 327 * @return the blinking pattern 328 */ 329 default LEDPattern synchronizedBlink(BooleanSupplier signal) { 330 HAL.report(tResourceType.kResourceType_LEDPattern, 1); 331 return (reader, writer) -> { 332 if (signal.getAsBoolean()) { 333 applyTo(reader, writer); 334 } else { 335 kOff.applyTo(reader, writer); 336 } 337 }; 338 } 339 340 /** 341 * Creates a pattern that brightens and dims this one over time. Brightness follows a sinusoidal 342 * pattern. 343 * 344 * @param period how fast the breathing pattern should complete a single cycle 345 * @return the breathing pattern 346 */ 347 default LEDPattern breathe(Time period) { 348 final long periodMicros = (long) period.in(Microseconds); 349 350 HAL.report(tResourceType.kResourceType_LEDPattern, 1); 351 return (reader, writer) -> { 352 applyTo( 353 reader, 354 (i, r, g, b) -> { 355 // How far we are in the cycle, in the range [0, 1) 356 double t = (RobotController.getTime() % periodMicros) / (double) periodMicros; 357 double phase = t * 2 * Math.PI; 358 359 // Apply the cosine function and shift its output from [-1, 1] to [0, 1] 360 // Use cosine so the period starts at 100% brightness 361 double dim = (Math.cos(phase) + 1) / 2.0; 362 363 int output = Color.lerpRGB(0, 0, 0, r, g, b, dim); 364 365 writer.setRGB( 366 i, 367 Color.unpackRGB(output, Color.RGBChannel.kRed), 368 Color.unpackRGB(output, Color.RGBChannel.kGreen), 369 Color.unpackRGB(output, Color.RGBChannel.kBlue)); 370 }); 371 }; 372 } 373 374 /** 375 * Creates a pattern that plays this pattern overlaid on another. Anywhere this pattern sets an 376 * LED to off (or {@link Color#kBlack}), the base pattern will be displayed instead. 377 * 378 * @param base the base pattern to overlay on top of 379 * @return the combined overlay pattern 380 */ 381 default LEDPattern overlayOn(LEDPattern base) { 382 HAL.report(tResourceType.kResourceType_LEDPattern, 1); 383 return (reader, writer) -> { 384 // write the base pattern down first... 385 base.applyTo(reader, writer); 386 387 // ... then, overwrite with the illuminated LEDs from the overlay 388 applyTo( 389 reader, 390 (i, r, g, b) -> { 391 if (r != 0 || g != 0 || b != 0) { 392 writer.setRGB(i, r, g, b); 393 } 394 }); 395 }; 396 } 397 398 /** 399 * Creates a pattern that displays outputs as a combination of this pattern and another. Color 400 * values are calculated as the average color of both patterns; if both patterns set the same LED 401 * to the same color, then it is set to that color, but if one pattern sets to one color and the 402 * other pattern sets it to off, then it will show the color of the first pattern but at 403 * approximately half brightness. This is different from {@link #overlayOn}, which will show the 404 * base pattern at full brightness if the overlay is set to off at that position. 405 * 406 * @param other the pattern to blend with 407 * @return the blended pattern 408 */ 409 default LEDPattern blend(LEDPattern other) { 410 HAL.report(tResourceType.kResourceType_LEDPattern, 1); 411 return (reader, writer) -> { 412 applyTo(reader, writer); 413 414 other.applyTo( 415 reader, 416 (i, r, g, b) -> { 417 int blendedRGB = 418 Color.lerpRGB( 419 reader.getRed(i), reader.getGreen(i), reader.getBlue(i), r, g, b, 0.5); 420 421 writer.setRGB( 422 i, 423 Color.unpackRGB(blendedRGB, Color.RGBChannel.kRed), 424 Color.unpackRGB(blendedRGB, Color.RGBChannel.kGreen), 425 Color.unpackRGB(blendedRGB, Color.RGBChannel.kBlue)); 426 }); 427 }; 428 } 429 430 /** 431 * Similar to {@link #blend(LEDPattern)}, but performs a bitwise mask on each color channel rather 432 * than averaging the colors for each LED. This can be helpful for displaying only a portion of 433 * the base pattern by applying a mask that sets the desired area to white, and all other areas to 434 * black. However, it can also be used to display only certain color channels or hues; for 435 * example, masking with {@code LEDPattern.color(Color.kRed)} will turn off the green and blue 436 * channels on the output pattern, leaving only the red LEDs to be illuminated. 437 * 438 * @param mask the mask to apply 439 * @return the masked pattern 440 */ 441 default LEDPattern mask(LEDPattern mask) { 442 HAL.report(tResourceType.kResourceType_LEDPattern, 1); 443 return (reader, writer) -> { 444 // Apply the current pattern down as normal... 445 applyTo(reader, writer); 446 447 mask.applyTo( 448 reader, 449 (i, r, g, b) -> { 450 // ... then perform a bitwise AND operation on each channel to apply the mask 451 writer.setRGB(i, r & reader.getRed(i), g & reader.getGreen(i), b & reader.getBlue(i)); 452 }); 453 }; 454 } 455 456 /** 457 * Creates a pattern that plays this one, but at a different brightness. Brightness multipliers 458 * are applied per-channel in the RGB space; no HSL or HSV conversions are applied. Multipliers 459 * are also uncapped, which may result in the original colors washing out and appearing less 460 * saturated or even just a bright white. 461 * 462 * <p>This method is predominantly intended for dimming LEDs to avoid painfully bright or 463 * distracting patterns from playing (apologies to the 2024 NE Greater Boston field staff). 464 * 465 * <p>For example, dimming can be done simply by adding a call to `atBrightness` at the end of a 466 * pattern: 467 * 468 * <pre> 469 * // Solid red, but at 50% brightness 470 * LEDPattern.solid(Color.kRed).atBrightness(Percent.of(50)); 471 * 472 * // Solid white, but at only 10% (i.e. ~0.5V) 473 * LEDPattern.solid(Color.kWhite).atBrightness(Percent.of(10)); 474 * </pre> 475 * 476 * @param relativeBrightness the multiplier to apply to all channels to modify brightness 477 * @return the input pattern, displayed at 478 */ 479 default LEDPattern atBrightness(Dimensionless relativeBrightness) { 480 double multiplier = relativeBrightness.in(Value); 481 482 HAL.report(tResourceType.kResourceType_LEDPattern, 1); 483 return (reader, writer) -> { 484 applyTo( 485 reader, 486 (i, r, g, b) -> { 487 // Clamp RGB values to keep them in the range [0, 255]. 488 // Otherwise, the casts to byte would result in values like 256 wrapping to 0 489 490 writer.setRGB( 491 i, 492 (int) MathUtil.clamp(r * multiplier, 0, 255), 493 (int) MathUtil.clamp(g * multiplier, 0, 255), 494 (int) MathUtil.clamp(b * multiplier, 0, 255)); 495 }); 496 }; 497 } 498 499 /** A pattern that turns off all LEDs. */ 500 LEDPattern kOff = solid(Color.kBlack); 501 502 /** 503 * Creates a pattern that displays a single static color along the entire length of the LED strip. 504 * 505 * @param color the color to display 506 * @return the pattern 507 */ 508 static LEDPattern solid(Color color) { 509 HAL.report(tResourceType.kResourceType_LEDPattern, 1); 510 return (reader, writer) -> { 511 int bufLen = reader.getLength(); 512 for (int led = 0; led < bufLen; led++) { 513 writer.setLED(led, color); 514 } 515 }; 516 } 517 518 /** 519 * Creates a pattern that works as a mask layer for {@link #mask(LEDPattern)} that illuminates 520 * only the portion of the LED strip corresponding with some progress. The mask pattern will start 521 * from the base and set LEDs to white at a proportion equal to the progress returned by the 522 * function. Some usages for this could be for displaying progress of a flywheel to its target 523 * velocity, progress of a complex autonomous sequence, or the height of an elevator. 524 * 525 * <p>For example, creating a mask for displaying a red-to-blue gradient, starting from the red 526 * end, based on where an elevator is in its range of travel. 527 * 528 * <pre> 529 * LEDPattern basePattern = gradient(Color.kRed, Color.kBlue); 530 * LEDPattern progressPattern = 531 * basePattern.mask(progressMaskLayer(() -> elevator.getHeight() / elevator.maxHeight()); 532 * </pre> 533 * 534 * @param progressSupplier the function to call to determine the progress. This should return 535 * values in the range [0, 1]; any values outside that range will be clamped. 536 * @return the mask pattern 537 */ 538 static LEDPattern progressMaskLayer(DoubleSupplier progressSupplier) { 539 HAL.report(tResourceType.kResourceType_LEDPattern, 1); 540 return (reader, writer) -> { 541 double progress = MathUtil.clamp(progressSupplier.getAsDouble(), 0, 1); 542 543 int bufLen = reader.getLength(); 544 int max = (int) (bufLen * progress); 545 546 for (int led = 0; led < max; led++) { 547 writer.setLED(led, Color.kWhite); 548 } 549 550 for (int led = max; led < bufLen; led++) { 551 writer.setLED(led, Color.kBlack); 552 } 553 }; 554 } 555 556 /** 557 * Display a set of colors in steps across the length of the LED strip. No interpolation is done 558 * between colors. Colors are specified by the first LED on the strip to show that color. The last 559 * color in the map will be displayed all the way to the end of the strip. LEDs positioned before 560 * the first specified step will be turned off (you can think of this as if there's a 0 -> black 561 * step by default) 562 * 563 * <pre> 564 * // Display red from 0-33%, white from 33% - 67%, and blue from 67% to 100% 565 * steps(Map.of(0.00, Color.kRed, 0.33, Color.kWhite, 0.67, Color.kBlue)) 566 * 567 * // Half off, half on 568 * steps(Map.of(0.5, Color.kWhite)) 569 * </pre> 570 * 571 * @param steps a map of progress to the color to start displaying at that position along the LED 572 * strip 573 * @return a motionless step pattern 574 */ 575 static LEDPattern steps(Map<? extends Number, Color> steps) { 576 HAL.report(tResourceType.kResourceType_LEDPattern, 1); 577 if (steps.isEmpty()) { 578 // no colors specified 579 DriverStation.reportWarning("Creating LED steps with no colors!", false); 580 return kOff; 581 } 582 583 if (steps.size() == 1 && steps.keySet().iterator().next().doubleValue() == 0) { 584 // only one color specified, just show a static color 585 DriverStation.reportWarning("Creating LED steps with only one color!", false); 586 return solid(steps.values().iterator().next()); 587 } 588 589 return (reader, writer) -> { 590 int bufLen = reader.getLength(); 591 592 // precompute relevant positions for this buffer so we don't need to do a check 593 // on every single LED index 594 var stopPositions = new LongToObjectHashMap<Color>(); 595 steps.forEach( 596 (progress, color) -> { 597 stopPositions.put((int) Math.floor(progress.doubleValue() * bufLen), color); 598 }); 599 600 Color currentColor = Color.kBlack; 601 for (int led = 0; led < bufLen; led++) { 602 currentColor = Objects.requireNonNullElse(stopPositions.get(led), currentColor); 603 604 writer.setLED(led, currentColor); 605 } 606 }; 607 } 608 609 /** Types of gradients. */ 610 enum GradientType { 611 /** 612 * A continuous gradient, where the gradient wraps around to allow for seamless scrolling 613 * effects. 614 */ 615 kContinuous, 616 617 /** 618 * A discontinuous gradient, where the first pixel is set to the first color of the gradient and 619 * the final pixel is set to the last color of the gradient. There is no wrapping effect, so 620 * scrolling effects will display an obvious seam. 621 */ 622 kDiscontinuous 623 } 624 625 /** 626 * Creates a pattern that displays a non-animated gradient of colors across the entire length of 627 * the LED strip. Colors are evenly distributed along the full length of the LED strip. The 628 * gradient type is configured with the {@code type} parameter, allowing the gradient to be either 629 * continuous (no seams, good for scrolling effects) or discontinuous (a clear seam is visible, 630 * but the gradient applies to the full length of the LED strip without needing to use some space 631 * for wrapping). 632 * 633 * @param type the type of gradient (continuous or discontinuous) 634 * @param colors the colors to display in the gradient 635 * @return a motionless gradient pattern 636 */ 637 static LEDPattern gradient(GradientType type, Color... colors) { 638 HAL.report(tResourceType.kResourceType_LEDPattern, 1); 639 if (colors.length == 0) { 640 // Nothing to display 641 DriverStation.reportWarning("Creating a gradient with no colors!", false); 642 return kOff; 643 } 644 645 if (colors.length == 1) { 646 // No gradients with one color 647 DriverStation.reportWarning("Creating a gradient with only one color!", false); 648 return solid(colors[0]); 649 } 650 651 final int numSegments = colors.length; 652 653 return (reader, writer) -> { 654 int bufLen = reader.getLength(); 655 int ledsPerSegment = 656 switch (type) { 657 case kContinuous -> bufLen / numSegments; 658 case kDiscontinuous -> (bufLen - 1) / (numSegments - 1); 659 }; 660 661 for (int led = 0; led < bufLen; led++) { 662 int colorIndex = (led / ledsPerSegment) % numSegments; 663 int nextColorIndex = (colorIndex + 1) % numSegments; 664 double t = (led / (double) ledsPerSegment) % 1; 665 666 Color color = colors[colorIndex]; 667 Color nextColor = colors[nextColorIndex]; 668 int gradientColor = 669 Color.lerpRGB( 670 color.red, 671 color.green, 672 color.blue, 673 nextColor.red, 674 nextColor.green, 675 nextColor.blue, 676 t); 677 678 writer.setRGB( 679 led, 680 Color.unpackRGB(gradientColor, Color.RGBChannel.kRed), 681 Color.unpackRGB(gradientColor, Color.RGBChannel.kGreen), 682 Color.unpackRGB(gradientColor, Color.RGBChannel.kBlue)); 683 } 684 }; 685 } 686 687 /** 688 * Creates an LED pattern that displays a rainbow across the color wheel. The rainbow pattern will 689 * stretch across the entire length of the LED strip. 690 * 691 * @param saturation the saturation of the HSV colors, in [0, 255] 692 * @param value the value of the HSV colors, in [0, 255] 693 * @return the rainbow pattern 694 */ 695 static LEDPattern rainbow(int saturation, int value) { 696 HAL.report(tResourceType.kResourceType_LEDPattern, 1); 697 return (reader, writer) -> { 698 int bufLen = reader.getLength(); 699 for (int i = 0; i < bufLen; i++) { 700 int hue = ((i * 180) / bufLen) % 180; 701 writer.setHSV(i, hue, saturation, value); 702 } 703 }; 704 } 705}