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}