[copybara] Sync with robotpy (#8964)

GitOrigin-RevId: 9dff8f977401e78be0bb6f39cea2328320ab2d95
This commit is contained in:
PJ Reiniger
2026-06-08 22:22:48 -04:00
committed by GitHub
parent 0213ecf382
commit 111130d8bb
20 changed files with 1026 additions and 265 deletions

View File

@@ -36,6 +36,12 @@ class WaitUntilCommand(Command):
guarantee that the time at which the action is performed will be judged to be legal by the guarantee that the time at which the action is performed will be judged to be legal by the
referees. When in doubt, add a safety factor or time the action manually. referees. When in doubt, add a safety factor or time the action manually.
The match time counts down when connected to FMS or the DS is in practice mode for the
current mode. When the DS is not connected to FMS or in practice mode, the command will not
wait.
see :func:`wpilib.DriverStation.GetMatchTime`
:param time: the match time at which to end, in seconds :param time: the match time at which to end, in seconds
""" """
... ...
@@ -48,7 +54,7 @@ class WaitUntilCommand(Command):
self._condition = condition self._condition = condition
def init_time(time: float) -> None: def init_time(time: float) -> None:
self._condition = lambda: Timer.getMatchTime() - time > 0 self._condition = lambda: Timer.getMatchTime() < time
num_args = len(args) + len(kwargs) num_args = len(args) + len(kwargs)

View File

@@ -50,6 +50,7 @@ scan_headers_ignore = [
# TODO: might want this in the future # TODO: might want this in the future
"mrc/*", "mrc/*",
"mrclib/*",
"src/ds_types_fmt.h", "src/ds_types_fmt.h",
"sim_cb.h", "sim_cb.h",

View File

@@ -1,174 +0,0 @@
Guidelines for porting WPILib examples to Python
================================================
To ensure that our examples are helpful and accurate for those learning how to
use RobotPy, we have a set of guidelines for adding new examples to the project.
These guidelines are not strictly enforced, but we do ask that you follow them
when submitting pull requests with new examples. This will make the review
process easier for everyone involved.
In general, our examples are based on the Java examples from allwpilib, as Java
is often easier to translate to Python. However, not all of our existing
examples adhere to all of these guidelines. If you see an opportunity to improve
an existing example, feel free to make the necessary changes.
Shorter thoughts
----------------
Testing:
* New examples must run! You *must* test your code on either a robot or in
simulation. If there's something broken in RobotPy, file an issue to get it
fixed
* You can find instructions on how to test a vision file [here](https://robotpy.readthedocs.io/en/stable/vision/other.html#vision-other-runcustom)!
* Format your code with black
General:
* We always try to stay as close to the original examples as possible
* `Main.java` is never needed
* Don't ever check in files for your IDE (.vscode, .idea, etc)
* Copy over the copyright statement from the original file
Naming:
* Filenames should always be all lowercase
* Function names are camelCase
* Class names start with a capital letter
* Class method names are camelCase
* Class member variables such as `m_name` should be `self.name` in Python
* Protected/private methods/members can optionally be prefixed with `_`
Misc conversion thoughts
* Comparisons to null such as `foo == null` become `foo is None`
* Single-line lambdas can be converted to python lambda statements. Anything
longer needs to be a separate function somewhere
* Never modify `sys.path` directly!
Longer thoughts
---------------
Never initialize anything other than constants at class/global scope. Here are
a few examples:
```python
# OK: just a constant
MY_CONSTANT = 42
# BAD: at global scope
motor = wpilib.Talon(1)
class MyRobot:
# BAD: at class scope
motor = wpilib.Talon(1)
def __init__(self):
# OK: variable assigned to `self`
self.motor = wpilib.Talon(1)
```
---
Import order doesn't really matter, but we prefer the following convention:
```python
# Import things from the python standard library first
import os
import typing
# Import things from robotpy in a second group
import wpilib
import commands2
# Import things from the other files in the example last
import constants
import subsystems.drivetrain
```
---
The `pass` statement is only required for empty functions:
```python
# OK
def empty_function():
pass
def has_docstring():
"""Some docstring"""
pass # NOT NEEDED
class C:
def __init__(self):
super().__init__()
pass # NOT NEEDED
```
Include all the comments
------------------------
**IMPORTANT**: Include all the comments from the existing examples. These
comments provide helpful explanations and context for the code.
Converting Java comments to Python docstrings can be tedious and error prone. We
have a tool at https://github.com/robotpy/devtools/blob/main/sphinxify_server.py
that launches an HTML page that you can just paste doxygen or javadoc comments
into and it will convert it to a mostly usable docstring.
```python
# 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.
"""
Some docstring describing what this file does
"""
class SomeClass:
"""
This describes what this class does
"""
def __init__(self):
"""
This describes what the constructor does
"""
def myFunction(self, a: int) -> int:
"""
This function is great.
:param a: Input parameter a
"""
```
Command-based robot specific things
-----------------------------------
We use `commands2.TimedCommandRobot` instead of TimedRobot. It provides a
`robotPeriodic` method for you, so it doesn't need to be included from
the java code unless robotPeriodic function does something other than
run the command scheduler.
Java examples will often have a `Constants.java` file with a bunch of constants
in it. RobotPy examples will put those constants in a `constants.py` as globals.
To group constants sometimes it makes sense to put each group in its own class,
but a single `Constants` class should be avoided.
Final thoughts
--------------
Before translating WPILib Java code to RobotPy's WPILib, first take some time
and read through the existing RobotPy code to get a feel for the style of the
code. Try to keep it Pythonic and yet true to the original spirit of the code.
Style *does* matter, as students will be reading through this code and it will
potentially influence their decisions in the future.
Remember, all contributions are welcome, no matter how big or small!

View File

@@ -1,79 +0,0 @@
#!/usr/bin/env python3
#
# 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.
#
from pathlib import Path
def check_file_content(file_path):
with open(file_path, "r") as file:
lines = file.readlines()
if file.name.endswith("robot.py"):
expected_lines = [
"#!/usr/bin/env python3\n",
"#\n",
"# Copyright (c) FIRST and other WPILib contributors.\n",
"# Open Source Software; you can modify and/or share it under the terms of\n",
"# the WPILib BSD license file in the root directory of this project.\n",
"#\n",
"\n",
]
else:
expected_lines = [
"#\n",
"# Copyright (c) FIRST and other WPILib contributors.\n",
"# Open Source Software; you can modify and/or share it under the terms of\n",
"# the WPILib BSD license file in the root directory of this project.\n",
"#\n",
"\n",
]
if lines[: len(expected_lines)] != expected_lines:
print(
"\n".join(
[
f"{file_path}",
"ERROR: File must start with the following lines",
"------------------------------",
"".join(expected_lines[:-1]),
"------------------------------",
"Found:",
"".join(lines[: len(expected_lines)]),
"------------------------------",
]
)
)
return False
return True
def main():
current_directory = Path(__file__).parent
python_files = [
x
for x in current_directory.glob("./**/*.py")
if not x.parent == current_directory and x.stat().st_size != 0
]
non_compliant_files = [
file for file in python_files if not check_file_content(file)
]
non_compliant_files.sort()
if non_compliant_files:
print("Non-compliant files:")
for file in non_compliant_files:
print(f"- {file}")
exit(1) # Exit with an error code
else:
print("All files are compliant.")
if __name__ == "__main__":
main()

View File

@@ -18,7 +18,7 @@ class MyRobot(wpilib.TimedRobot):
self.leftDrive = wpilib.PWMSparkMax(0) self.leftDrive = wpilib.PWMSparkMax(0)
self.rightDrive = wpilib.PWMSparkMax(1) self.rightDrive = wpilib.PWMSparkMax(1)
self.robotDrive = wpilib.DifferentialDrive(self.leftDrive, self.rightDrive) self.robotDrive = wpilib.DifferentialDrive(self.leftDrive, self.rightDrive)
self.controller = wpilib.NiDsXboxController(0) self.controller = wpilib.Gamepad(0)
self.timer = wpilib.Timer() self.timer = wpilib.Timer()
# We need to invert one side of the drivetrain so that positive voltages # We need to invert one side of the drivetrain so that positive voltages

View File

@@ -18,7 +18,7 @@ class MyRobot(wpilib.TimedRobot):
# Reuse buffer # Reuse buffer
# Default to a length of 60 # Default to a length of 60
self.ledData = [wpilib.AddressableLED.LEDData() for _ in range(60)] self.ledData = wpilib.AddressableLEDBuffer(60)
self.led.setLength(len(self.ledData)) self.led.setLength(len(self.ledData))
# Set the data # Set the data

View File

@@ -234,6 +234,7 @@ PKG_CONFIG_DEPS = [
generate_robotpy_pybind_build_info( generate_robotpy_pybind_build_info(
name = "robotpy-wpilib-generator", name = "robotpy-wpilib-generator",
additional_srcs = [ additional_srcs = [
"src/main/python/wpilib/src/rpy/AddressableLEDBuffer.h",
"src/main/python/wpilib/src/rpy/Filesystem.h", "src/main/python/wpilib/src/rpy/Filesystem.h",
"src/main/python/wpilib/src/rpy/Notifier.h", "src/main/python/wpilib/src/rpy/Notifier.h",
"src/main/python/wpilib/src/rpy/MotorControllerGroup.h", "src/main/python/wpilib/src/rpy/MotorControllerGroup.h",

View File

@@ -36,6 +36,17 @@ def wpilib_extension(srcs = [], header_to_dat_deps = [], extra_hdrs = [], includ
("wpi::PyNotifier", "wpi__PyNotifier.hpp"), ("wpi::PyNotifier", "wpi__PyNotifier.hpp"),
], ],
), ),
struct(
class_name = "AddressableLEDBuffer",
yml_file = "semiwrap/AddressableLEDBuffer.yml",
header_root = "wpilibc/src/main/python/wpilib/src",
header_file = "wpilibc/src/main/python/wpilib/src/rpy/AddressableLEDBuffer.h",
tmpl_class_names = [],
trampolines = [
("wpi::AddressableLEDBuffer", "wpi__AddressableLEDBuffer.hpp"),
("wpi::AddressableLEDBuffer::View", "wpi__AddressableLEDBuffer__View.hpp"),
],
),
struct( struct(
class_name = "EdgeConfiguration", class_name = "EdgeConfiguration",
yml_file = "semiwrap/EdgeConfiguration.yml", yml_file = "semiwrap/EdgeConfiguration.yml",

View File

@@ -94,6 +94,8 @@ DYNAMIC_CAMERA_SERVER = 1
Filesystem = "rpy/Filesystem.h" Filesystem = "rpy/Filesystem.h"
MotorControllerGroup = "rpy/MotorControllerGroup.h" MotorControllerGroup = "rpy/MotorControllerGroup.h"
Notifier = "rpy/Notifier.h" Notifier = "rpy/Notifier.h"
# rpy only
AddressableLEDBuffer = "rpy/AddressableLEDBuffer.h"
# wpi/counter # wpi/counter
EdgeConfiguration = "wpi/counter/EdgeConfiguration.hpp" EdgeConfiguration = "wpi/counter/EdgeConfiguration.hpp"

View File

@@ -1,3 +1,6 @@
extra_includes:
- rpy/AddressableLEDBuffer.h
functions: functions:
format_as: format_as:
ignore: true ignore: true
@@ -19,6 +22,10 @@ classes:
SetGlobalData: SetGlobalData:
enums: enums:
ColorOrder: ColorOrder:
inline_code: |
.def("setData", [](wpi::AddressableLED& self, const wpi::AddressableLEDBuffer& data) {
return self.SetData(data);
}, release_gil(), py::prepend());
wpi::AddressableLED::LEDData: wpi::AddressableLED::LEDData:
force_no_trampoline: true force_no_trampoline: true
ignored_bases: ignored_bases:

View File

@@ -0,0 +1,58 @@
classes:
wpi::AddressableLEDBuffer:
methods:
AddressableLEDBuffer:
SetRGB:
SetHSV:
SetLED:
overloads:
size_t, const wpi::util::Color&:
size_t, const wpi::util::Color8Bit&:
size:
rename: __len__
GetRed:
GetGreen:
GetBlue:
GetLED:
GetLED8Bit:
at:
rename: __getitem__
begin:
ignore: true
end:
ignore: true
CreateView:
rename: __getitem__
no_release_gil: true
keepalive:
- [0, 1]
inline_code: |
.def("__iter__", [](wpi::AddressableLEDBuffer& self) {
return py::make_iterator(self.begin(), self.end());
}, py::keep_alive<0, 1>())
wpi::AddressableLEDBuffer::View:
methods:
size:
rename: __len__
SetRGB:
SetHSV:
SetLED:
overloads:
size_t, const wpi::util::Color&:
size_t, const wpi::util::Color8Bit&:
at:
rename: __getitem__
overloads:
size_t:
size_t [const]:
ignore: true
begin:
ignore: true
end:
ignore: true
GetLED:
GetLED8Bit:
inline_code: |
.def("__iter__", [](wpi::AddressableLEDBuffer::View& self) {
return py::make_iterator(self.begin(), self.end());
}, py::keep_alive<0, 1>())

View File

@@ -1,3 +1,6 @@
extra_includes:
- rpy/AddressableLEDBuffer.h
classes: classes:
wpi::LEDPattern: wpi::LEDPattern:
enums: enums:
@@ -8,8 +11,16 @@ classes:
ApplyTo: ApplyTo:
overloads: overloads:
std::span<wpi::AddressableLED::LEDData> [const]: std::span<wpi::AddressableLED::LEDData> [const]:
cpp_code: |
[](const wpi::LEDPattern& self, wpi::AddressableLEDBuffer::View data) {
return self.ApplyTo(data);
}
LEDReader, std::function<void (int, wpi::util::Color)> [const]: LEDReader, std::function<void (int, wpi::util::Color)> [const]:
std::span<wpi::AddressableLED::LEDData>, std::function<void (int, wpi::util::Color)> [const]: std::span<wpi::AddressableLED::LEDData>, std::function<void (int, wpi::util::Color)> [const]:
cpp_code: |
[](const wpi::LEDPattern& self, wpi::AddressableLEDBuffer::View data, std::function<void(int, wpi::util::Color)> writer) {
return self.ApplyTo(data, writer);
}
Reversed: Reversed:
OffsetBy: OffsetBy:
ScrollAtRelativeVelocity: ScrollAtRelativeVelocity:
@@ -38,6 +49,14 @@ classes:
ignore: true ignore: true
Rainbow: Rainbow:
MapIndex: MapIndex:
inline_code: |
.def("applyTo", [](const wpi::LEDPattern& self, wpi::AddressableLEDBuffer& data) {
self.ApplyTo(static_cast<std::span<wpi::AddressableLED::LEDData>>(data));
}, py::arg("data"), release_gil())
.def("applyTo", [](const wpi::LEDPattern& self, wpi::AddressableLEDBuffer& data,
std::function<void (int, wpi::util::Color)> writer) {
self.ApplyTo(static_cast<std::span<wpi::AddressableLED::LEDData>>(data), std::move(writer));
}, py::arg("data"), py::arg("writer").none(false), release_gil())
wpi::LEDPattern::LEDReader: wpi::LEDPattern::LEDReader:
methods: methods:
LEDReader: LEDReader:

View File

@@ -4,6 +4,7 @@ from . import _init__wpilib
from ._wpilib import ( from ._wpilib import (
ADXL345_I2C, ADXL345_I2C,
AddressableLED, AddressableLED,
AddressableLEDBuffer,
Alert, Alert,
Alliance, Alliance,
AnalogAccelerometer, AnalogAccelerometer,
@@ -117,6 +118,7 @@ from ._wpilib import (
__all__ = [ __all__ = [
"ADXL345_I2C", "ADXL345_I2C",
"AddressableLED", "AddressableLED",
"AddressableLEDBuffer",
"Alert", "Alert",
"Alliance", "Alliance",
"AnalogAccelerometer", "AnalogAccelerometer",

View File

@@ -177,7 +177,7 @@ class RobotStarter:
for i in range(100): for i in range(100):
if ( if (
inst.getNetworkMode() inst.getNetworkMode()
& ntcore.NetworkTableInstance.NetworkMode.kNetModeStarting & ntcore.NetworkTableInstance.NetworkMode.STARTING.value
) == 0: ) == 0:
break break
# real sleep since we're waiting for the server, not simulated sleep # real sleep since we're waiting for the server, not simulated sleep

View File

@@ -0,0 +1,129 @@
// 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 "rpy/AddressableLEDBuffer.h"
#include <stdexcept>
#include <span>
namespace wpi {
void AddressableLEDBuffer::SetRGB(size_t index, int r, int g, int b) {
m_buffer.at(index).SetRGB(r, g, b);
}
void AddressableLEDBuffer::SetHSV(size_t index, int h, int s, int v) {
m_buffer.at(index).SetHSV(h, s, v);
}
void AddressableLEDBuffer::SetLED(size_t index, const wpi::util::Color& color) {
m_buffer.at(index).SetLED(color);
}
void AddressableLEDBuffer::SetLED(size_t index, const wpi::util::Color8Bit& color) {
m_buffer.at(index).SetLED(color);
}
int AddressableLEDBuffer::GetRed(size_t index) const { return m_buffer.at(index).r; }
int AddressableLEDBuffer::GetGreen(size_t index) const {
return m_buffer.at(index).g;
}
int AddressableLEDBuffer::GetBlue(size_t index) const { return m_buffer.at(index).b; }
wpi::util::Color AddressableLEDBuffer::GetLED(size_t index) const {
const auto& led = m_buffer.at(index);
return wpi::util::Color{led.r / 255.0, led.g / 255.0, led.b / 255.0};
}
wpi::util::Color8Bit AddressableLEDBuffer::GetLED8Bit(size_t index) const {
const auto& led = m_buffer.at(index);
return wpi::util::Color8Bit{led.r, led.g, led.b};
}
AddressableLED::LEDData& AddressableLEDBuffer::at(size_t index) {
return m_buffer.at(index);
}
AddressableLED::LEDData& AddressableLEDBuffer::operator[](size_t index) {
return m_buffer.at(index);
}
const AddressableLED::LEDData& AddressableLEDBuffer::operator[](
size_t index) const {
return m_buffer.at(index);
}
void AddressableLEDBuffer::View::SetRGB(size_t index, int r, int g, int b) {
at(index).SetRGB(r, g, b);
}
void AddressableLEDBuffer::View::SetHSV(size_t index, int h, int s, int v) {
at(index).SetHSV(h, s, v);
}
void AddressableLEDBuffer::View::SetLED(size_t index, const wpi::util::Color& color) {
at(index).SetLED(color);
}
void AddressableLEDBuffer::View::SetLED(size_t index,
const wpi::util::Color8Bit& color) {
at(index).SetLED(color);
}
AddressableLED::LEDData& AddressableLEDBuffer::View::at(size_t index) {
// std::span::at doesn't exist until C++26
if (index >= m_data.size()) {
throw std::out_of_range("Index out of range");
}
return m_data[index];
}
AddressableLED::LEDData& AddressableLEDBuffer::View::operator[](
size_t index) {
return at(index);
}
const AddressableLED::LEDData& AddressableLEDBuffer::View::at(
size_t index) const {
// std::span::at doesn't exist until C++26
if (index >= m_data.size()) {
throw std::out_of_range("Index out of range");
}
return m_data[index];
}
const AddressableLED::LEDData& AddressableLEDBuffer::View::operator[](
size_t index) const {
return at(index);
}
wpi::util::Color AddressableLEDBuffer::View::GetLED(size_t index) const {
const auto& led = at(index);
return wpi::util::Color{led.r / 255.0, led.g / 255.0, led.b / 255.0};
}
wpi::util::Color8Bit AddressableLEDBuffer::View::GetLED8Bit(size_t index) const {
const auto& led = at(index);
return wpi::util::Color8Bit{led.r, led.g, led.b};
}
AddressableLEDBuffer::View::View(std::span<AddressableLED::LEDData> data)
: m_data(data) {}
AddressableLEDBuffer::View AddressableLEDBuffer::CreateView(
pybind11::slice slice) {
size_t start = 0, stop = 0, step = 0, slicelength = 0;
slice.compute(m_buffer.size(), &start, &stop, &step, &slicelength);
if (step != 1) {
throw std::out_of_range("step != 1");
}
if (!slicelength) {
throw std::out_of_range("zero length view");
}
return View(std::span(m_buffer).subspan(start, slicelength));
}
} // namespace wpi

View File

@@ -0,0 +1,285 @@
// 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.
#pragma once
#include <span>
#include <stdexcept>
#include <vector>
#include "pybind11/pytypes.h"
#include "wpi/hardware/led/AddressableLED.hpp"
#include "wpi/util/Color.hpp"
#include "wpi/util/Color8Bit.hpp"
namespace wpi {
/**
* Buffer storage for Addressable LEDs.
*/
class AddressableLEDBuffer {
public:
/**
* Constructs a new LED buffer with the specified length.
*
* @param length The length of the buffer in pixels
*/
explicit AddressableLEDBuffer(size_t length) : m_buffer(length) {}
/**
* Sets a specific LED in the buffer.
*
* @param index the index to write
* @param r the r value [0-255]
* @param g the g value [0-255]
* @param b the b value [0-255]
*/
void SetRGB(size_t index, int r, int g, int b);
/**
* Sets a specific LED in the buffer.
*
* @param index the index to write
* @param h the h value [0-180)
* @param s the s value [0-255]
* @param v the v value [0-255]
*/
void SetHSV(size_t index, int h, int s, int v);
/**
* Sets a specific LED in the buffer.
*
* @param index the index to write
* @param color the color to write
*/
void SetLED(size_t index, const wpi::util::Color& color);
/**
* Sets a specific LED in the buffer.
*
* @param index the index to write
* @param color the color to write
*/
void SetLED(size_t index, const wpi::util::Color8Bit& color);
/**
* Gets the buffer length.
*
* @return the buffer length
*/
size_t size() const { return m_buffer.size(); }
/**
* Gets the red value at the specified index.
*
* @param index the index
* @return the red value
*/
int GetRed(size_t index) const;
/**
* Gets the green value at the specified index.
*
* @param index the index
* @return the green value
*/
int GetGreen(size_t index) const;
/**
* Gets the blue value at the specified index.
*
* @param index the index
* @return the blue value
*/
int GetBlue(size_t index) const;
/**
* Gets the color at the specified index.
*
* @param index the index
* @return the LED color
*/
wpi::util::Color GetLED(size_t index) const;
/**
* Gets the color at the specified index.
*
* @param index the index
* @return the LED color
*/
wpi::util::Color8Bit GetLED8Bit(size_t index) const;
/**
* Implicit conversion to span of LED data
*/
operator std::span<wpi::AddressableLED::LEDData>() {
return std::span{m_buffer};
}
/**
* Implicit conversion to span of const LED data
*/
operator std::span<const wpi::AddressableLED::LEDData>() const {
return std::span{m_buffer};
}
/**
* Gets the LED data at the specified index.
*
* @param index the index
* @return reference to the LED data
*/
wpi::AddressableLED::LEDData& at(size_t index);
/**
* Gets the LED data at the specified index.
*
* @param index the index
* @return reference to the LED data
*/
wpi::AddressableLED::LEDData& operator[](size_t index);
/**
* Gets the LED data at the specified index.
*
* @param index the index
* @return const reference to the LED data
*/
const wpi::AddressableLED::LEDData& operator[](size_t index) const;
auto begin() { return m_buffer.begin(); }
auto end() { return m_buffer.end(); }
/**
* A view of another addressable LED buffer. Views provide an easy way to split a large LED
* strip into smaller sections that can be animated individually.
*/
class View {
public:
/**
* Gets the length of the view.
*/
size_t size() const { return m_data.size(); }
/**
* Sets a specific LED in the view.
*
* @param index the index to write
* @param r the r value [0-255]
* @param g the g value [0-255]
* @param b the b value [0-255]
*/
void SetRGB(size_t index, int r, int g, int b);
/**
* Sets a specific LED in the view.
*
* @param index the index to write
* @param h the h value [0-180)
* @param s the s value [0-255]
* @param v the v value [0-255]
*/
void SetHSV(size_t index, int h, int s, int v);
/**
* Sets a specific LED in the view.
*
* @param index the index to write
* @param color the color to write
*/
void SetLED(size_t index, const wpi::util::Color& color);
/**
* Sets a specific LED in the view.
*
* @param index the index to write
* @param color the color to write
*/
void SetLED(size_t index, const wpi::util::Color8Bit& color);
/**
* Gets the LED data at the specified index.
*
* @param index the index
* @return reference to the LED data
*/
wpi::AddressableLED::LEDData& at(size_t index);
/**
* Gets the LED data at the specified index.
*
* @param index the index
* @return reference to the LED data
*/
wpi::AddressableLED::LEDData& operator[](size_t index);
/**
* Gets the LED data at the specified index.
*
* @param index the index
* @return const reference to the LED data
*/
const wpi::AddressableLED::LEDData& at(size_t index) const;
/**
* Gets the LED data at the specified index.
*
* @param index the index
* @return const reference to the LED data
*/
const wpi::AddressableLED::LEDData& operator[](size_t index) const;
auto begin() { return m_data.begin(); }
auto end() { return m_data.end(); }
/**
* Gets the color at the specified index.
*
* @param index the index
* @return the LED color
*/
wpi::util::Color GetLED(size_t index) const;
/**
* Gets the color at the specified index.
*
* @param index the index
* @return the LED color
*/
wpi::util::Color8Bit GetLED8Bit(size_t index) const;
/**
* Implicit conversion to span of LED data
*/
operator std::span<wpi::AddressableLED::LEDData>() {
return m_data;
}
/**
* Implicit conversion to span of const LED data
*/
operator std::span<const wpi::AddressableLED::LEDData>() const {
return m_data;
}
private:
friend class AddressableLEDBuffer;
explicit View(std::span<wpi::AddressableLED::LEDData> data);
std::span<wpi::AddressableLED::LEDData> m_data;
};
/**
* Creates a read/write view of this buffer.
*
* @param slice the desired slice of the buffer (e.g. 2:4), step must be unspecified or 1
* @return View object representing the view
* @throws std::out_of_range if the view would exceed buffer bounds
*/
View CreateView(pybind11::slice slice);
private:
std::vector<wpi::AddressableLED::LEDData> m_buffer;
};
} // namespace frc

View File

@@ -0,0 +1,164 @@
# 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.
import pytest
from wpilib import AddressableLEDBuffer
from wpiutil import Color, Color8Bit
AddressableLEDBufferView = AddressableLEDBuffer.View
class TestAddressableLEDBuffer:
"""Tests for AddressableLEDBuffer"""
@pytest.mark.parametrize(
"h,s,v,r,g,b",
[
(0, 0, 0, 0, 0, 0), # Black
(0, 0, 255, 255, 255, 255), # White
(0, 255, 255, 255, 0, 0), # Red
(60, 255, 255, 0, 255, 0), # Lime
(120, 255, 255, 0, 0, 255), # Blue
(30, 255, 255, 255, 255, 0), # Yellow
(90, 255, 255, 0, 255, 255), # Cyan
(150, 255, 255, 255, 0, 255), # Magenta
(0, 0, 191, 191, 191, 191), # Silver
(0, 0, 128, 128, 128, 128), # Gray
(0, 255, 128, 128, 0, 0), # Maroon
(30, 255, 128, 128, 128, 0), # Olive
(60, 255, 128, 0, 128, 0), # Green
(150, 255, 128, 128, 0, 128), # Purple
(90, 255, 128, 0, 128, 128), # Teal
(120, 255, 128, 0, 0, 128), # Navy
],
)
def test_hsv_convert(self, h, s, v, r, g, b):
"""Test HSV to RGB conversion"""
buffer = AddressableLEDBuffer(length=1)
buffer.setHSV(0, h, s, v)
color = buffer.getLED8Bit(0)
assert color.red == r, "R value didn't match"
assert color.green == g, "G value didn't match"
assert color.blue == b, "B value didn't match"
def test_get_color(self):
"""Test getting colors from buffer"""
buffer = AddressableLEDBuffer(4)
denim_color_8bit = Color8Bit(Color.DENIM)
first_blue_color_8bit = Color8Bit(Color.FIRST_BLUE)
first_red_color_8bit = Color8Bit(Color.FIRST_RED)
buffer.setLED(0, Color.FIRST_BLUE)
buffer.setLED(1, denim_color_8bit)
buffer.setLED(2, Color.FIRST_RED)
buffer.setLED(3, Color.FIRST_BLUE)
assert buffer.getLED(0) == Color.FIRST_BLUE
assert buffer.getLED(1) == Color.DENIM
assert buffer.getLED(2) == Color.FIRST_RED
assert buffer.getLED(3) == Color.FIRST_BLUE
assert buffer.getLED8Bit(0) == first_blue_color_8bit
assert buffer.getLED8Bit(1) == denim_color_8bit
assert buffer.getLED8Bit(2) == first_red_color_8bit
assert buffer.getLED8Bit(3) == first_blue_color_8bit
def test_get_red(self):
"""Test getting red component"""
buffer = AddressableLEDBuffer(1)
buffer.setRGB(0, 127, 128, 129)
assert buffer.getRed(0) == 127
def test_get_green(self):
"""Test getting green component"""
buffer = AddressableLEDBuffer(1)
buffer.setRGB(0, 127, 128, 129)
assert buffer.getGreen(0) == 128
def test_get_blue(self):
"""Test getting blue component"""
buffer = AddressableLEDBuffer(1)
buffer.setRGB(0, 127, 128, 129)
assert buffer.getBlue(0) == 129
def test_iteration(self):
buffer = AddressableLEDBuffer(3)
buffer.setRGB(0, 1, 2, 3)
buffer.setRGB(1, 4, 5, 6)
buffer.setRGB(2, 7, 8, 9)
results = []
for led in buffer:
results.append((led.r, led.g, led.b))
assert len(results) == 3
assert results[0] == (1, 2, 3)
assert results[1] == (4, 5, 6)
assert results[2] == (7, 8, 9)
def test_iteration_on_empty_buffer(self):
buffer = AddressableLEDBuffer(0)
for led in buffer:
assert False, "Iterator should not return items on an empty buffer"
class TestAddressableLEDBufferView:
"""Tests for AddressableLEDBufferView"""
def test_single_led(self):
"""Test setting a single LED through a view"""
buffer = AddressableLEDBuffer(10)
view = buffer[5:6]
color = Color.AQUA
view.setLED(0, color)
assert buffer.getLED(5) == color
assert view.getLED(0) == color
def test_segment(self):
"""Test segment view"""
buffer = AddressableLEDBuffer(10)
view = buffer[2:9]
view.setLED(0, Color.AQUA)
assert buffer.getLED(2) == Color.AQUA
view.setLED(6, Color.AZURE)
assert buffer.getLED(8) == Color.AZURE
@pytest.mark.skip("reversed views are not implemented")
def test_manual_reversed(self):
"""Test manually reversed view"""
buffer = AddressableLEDBuffer(10)
view = buffer[8:1:-1]
# LED 0 in the view should write to LED 8 on the real buffer
view.setLED(0, Color.AQUA)
assert buffer.getLED(8) == Color.AQUA
# LED 6 in the view should write to LED 2 on the real buffer
view.setLED(6, Color.AZURE)
assert buffer.getLED(2) == Color.AZURE
@pytest.mark.skip("reversed views are not implemented")
def test_full_manual_reversed(self):
"""Test full manual reversed view"""
buffer = AddressableLEDBuffer(10)
view = buffer[9::-1]
view.setLED(0, Color.WHITE)
assert buffer.getLED(9) == Color.WHITE
buffer.setLED(8, Color.RED)
assert view.getLED(1) == Color.RED
@pytest.mark.skip("reversed views are not implemented")
def test_reversed(self):
"""Test reversed view"""
buffer = AddressableLEDBuffer(10)
view = buffer[:].reversed()
view.setLED(0, Color.WHITE)
assert buffer.getLED(9) == Color.WHITE
view.setLED(9, Color.RED)
assert buffer.getLED(0) == Color.RED

View File

@@ -0,0 +1,329 @@
# 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.
import math
import pytest
import wpimath.units as units
from wpilib import AddressableLEDBuffer, LEDPattern, RobotController
from wpiutil import Color, Color8Bit
def lerp_rgb(a: Color, b: Color, t: float) -> Color8Bit:
a8 = Color8Bit(a)
b8 = Color8Bit(b)
return Color8Bit(
int(a8.red + (b8.red - a8.red) * t),
int(a8.green + (b8.green - a8.green) * t),
int(a8.blue + (b8.blue - a8.blue) * t),
)
@pytest.fixture(autouse=True)
def restore_time_source():
RobotController.setTimeSource(lambda: 0)
yield
RobotController.setTimeSource(RobotController.getTime)
def test_apply_to_buffer_direct():
buffer = AddressableLEDBuffer(4)
LEDPattern.solid(Color.YELLOW).applyTo(buffer)
for i in range(len(buffer)):
assert buffer.getLED8Bit(i) == Color8Bit(Color.YELLOW)
def test_apply_to_view_direct():
buffer = AddressableLEDBuffer(6)
view = buffer[2:5]
LEDPattern.solid(Color.AQUA).applyTo(view)
assert buffer.getLED8Bit(1) == Color8Bit(Color.BLACK)
assert buffer.getLED8Bit(2) == Color8Bit(Color.AQUA)
assert buffer.getLED8Bit(3) == Color8Bit(Color.AQUA)
assert buffer.getLED8Bit(4) == Color8Bit(Color.AQUA)
assert buffer.getLED8Bit(5) == Color8Bit(Color.BLACK)
def test_solid_color():
buffer = AddressableLEDBuffer(99)
LEDPattern.solid(Color.YELLOW).applyTo(buffer)
for i in range(len(buffer)):
assert buffer.getLED8Bit(i) == Color8Bit(Color.YELLOW)
def test_gradient_0_sets_to_black():
pattern = LEDPattern.gradient(LEDPattern.GradientType.CONTINUOUS, [])
buffer = AddressableLEDBuffer(99)
for i in range(len(buffer)):
buffer.setRGB(i, 127, 128, 129)
pattern.applyTo(buffer)
for i in range(len(buffer)):
assert buffer.getLED8Bit(i) == Color8Bit(Color.BLACK)
def test_gradient_1_sets_to_solid():
pattern = LEDPattern.gradient(LEDPattern.GradientType.CONTINUOUS, [Color.YELLOW])
buffer = AddressableLEDBuffer(99)
pattern.applyTo(buffer)
for i in range(len(buffer)):
assert buffer.getLED8Bit(i) == Color8Bit(Color.YELLOW)
def test_continuous_gradient_2_colors():
pattern = LEDPattern.gradient(
LEDPattern.GradientType.CONTINUOUS, [Color.YELLOW, Color.PURPLE]
)
buffer = AddressableLEDBuffer(99)
pattern.applyTo(buffer)
assert buffer.getLED8Bit(0) == Color8Bit(Color.YELLOW)
assert buffer.getLED8Bit(25) == lerp_rgb(Color.YELLOW, Color.PURPLE, 25 / 49.0)
assert buffer.getLED8Bit(49) == Color8Bit(Color.PURPLE)
assert buffer.getLED8Bit(73) == lerp_rgb(Color.YELLOW, Color.PURPLE, 25 / 49.0)
assert buffer.getLED8Bit(98) == Color8Bit(Color.YELLOW)
def test_discontinuous_gradient_2_colors():
pattern = LEDPattern.gradient(
LEDPattern.GradientType.DISCONTINUOUS, [Color.YELLOW, Color.PURPLE]
)
buffer = AddressableLEDBuffer(99)
pattern.applyTo(buffer)
assert buffer.getLED8Bit(0) == Color8Bit(Color.YELLOW)
assert buffer.getLED8Bit(49) == lerp_rgb(Color.YELLOW, Color.PURPLE, 0.5)
assert buffer.getLED8Bit(98) == Color8Bit(Color.PURPLE)
def test_step_0_sets_to_black():
pattern = LEDPattern.steps([])
buffer = AddressableLEDBuffer(99)
for i in range(len(buffer)):
buffer.setRGB(i, 127, 128, 129)
pattern.applyTo(buffer)
for i in range(len(buffer)):
assert buffer.getLED8Bit(i) == Color8Bit(Color.BLACK)
def test_step_1_sets_to_solid():
pattern = LEDPattern.steps([(0.0, Color.YELLOW)])
buffer = AddressableLEDBuffer(99)
pattern.applyTo(buffer)
for i in range(len(buffer)):
assert buffer.getLED8Bit(i) == Color8Bit(Color.YELLOW)
def test_step_half_sets_to_half_off_half_color():
pattern = LEDPattern.steps([(0.5, Color.YELLOW)])
buffer = AddressableLEDBuffer(99)
pattern.applyTo(buffer)
for i in range(49):
assert buffer.getLED8Bit(i) == Color8Bit(Color.BLACK)
for i in range(49, len(buffer)):
assert buffer.getLED8Bit(i) == Color8Bit(Color.YELLOW)
def make_grayscale_pattern():
return LEDPattern(
lambda reader, writer: [
writer(led, Color(led % 256, led % 256, led % 256))
for led in range(reader.size())
]
)
@pytest.mark.skip(reason="Python bindings do not expose a way to mock wpi::util::Now()")
def test_scroll_forward():
buffer = AddressableLEDBuffer(256)
pattern = make_grayscale_pattern().scrollAtRelativeSpeed(units.hertz(1 / 256.0))
for time in range(10):
RobotController.setTimeSource(lambda t=time: t)
pattern.applyTo(buffer)
for led in range(len(buffer)):
ch = (led - time) % 256
assert buffer.getLED8Bit(led) == Color8Bit(ch, ch, ch)
@pytest.mark.skip(reason="Python bindings do not expose a way to mock wpi::util::Now()")
def test_scroll_backward():
buffer = AddressableLEDBuffer(256)
pattern = make_grayscale_pattern().scrollAtRelativeSpeed(units.hertz(-1 / 256.0))
for time in range(10):
RobotController.setTimeSource(lambda t=time: t)
pattern.applyTo(buffer)
for led in range(len(buffer)):
ch = (led + time) % 256
assert buffer.getLED8Bit(led) == Color8Bit(ch, ch, ch)
def test_rainbow_at_full_size():
buffer = AddressableLEDBuffer(180)
saturation = 255
value = 255
pattern = LEDPattern.rainbow(saturation, value)
pattern.applyTo(buffer)
for led in range(len(buffer)):
assert buffer.getLED8Bit(led) == Color8Bit(
Color.fromHSV(led, saturation, value)
)
def test_rainbow_odd_size():
buffer = AddressableLEDBuffer(127)
scale = 180.0 / len(buffer)
saturation = 73
value = 128
pattern = LEDPattern.rainbow(saturation, value)
pattern.applyTo(buffer)
for led in range(len(buffer)):
expected = Color8Bit(Color.fromHSV(int(led * scale), saturation, value))
assert buffer.getLED8Bit(led) == expected
def test_reverse_solid():
buffer = AddressableLEDBuffer(90)
pattern = LEDPattern.solid(Color.ROSY_BROWN).reversed()
pattern.applyTo(buffer)
for led in range(len(buffer)):
assert buffer.getLED8Bit(led) == Color8Bit(Color.ROSY_BROWN)
def test_reverse_steps():
buffer = AddressableLEDBuffer(100)
pattern = LEDPattern.steps([(0.0, Color.WHITE), (0.5, Color.YELLOW)]).reversed()
pattern.applyTo(buffer)
for led in range(len(buffer)):
expected = Color8Bit(Color.YELLOW if led < 50 else Color.WHITE)
assert buffer.getLED8Bit(led) == expected
def white_yellow_purple(reader, writer):
colors = [Color.WHITE, Color.YELLOW, Color.PURPLE]
for led in range(reader.size()):
writer(led, colors[led % 3])
@pytest.mark.parametrize(
"offset,expected",
[
(1, [Color.PURPLE, Color.WHITE, Color.YELLOW]),
(-1, [Color.YELLOW, Color.PURPLE, Color.WHITE]),
(0, [Color.WHITE, Color.YELLOW, Color.PURPLE]),
],
)
def test_offset_pattern(offset, expected):
buffer = AddressableLEDBuffer(21)
pattern = LEDPattern(white_yellow_purple).offsetBy(offset)
pattern.applyTo(buffer)
for led in range(len(buffer)):
assert buffer.getLED8Bit(led) == Color8Bit(expected[led % 3])
@pytest.mark.skip(reason="Python bindings do not expose a way to mock wpi::util::Now()")
def test_blink_symmetric():
pattern = LEDPattern.solid(Color.WHITE).blink(units.seconds(2))
buffer = AddressableLEDBuffer(1)
for t in range(8):
RobotController.setTimeSource(lambda tick=t: tick * 1_000_000)
pattern.applyTo(buffer)
expected = Color8Bit(Color.WHITE if t % 4 < 2 else Color.BLACK)
assert buffer.getLED8Bit(0) == expected
def test_blink_in_sync():
state = {"on": False}
pattern = LEDPattern.solid(Color.WHITE).synchronizedBlink(lambda: state["on"])
buffer = AddressableLEDBuffer(1)
pattern.applyTo(buffer)
assert buffer.getLED8Bit(0) == Color8Bit(Color.BLACK)
state["on"] = True
pattern.applyTo(buffer)
assert buffer.getLED8Bit(0) == Color8Bit(Color.WHITE)
state["on"] = False
pattern.applyTo(buffer)
assert buffer.getLED8Bit(0) == Color8Bit(Color.BLACK)
@pytest.mark.skip(reason="Python bindings do not expose a way to mock wpi::util::Now()")
def test_breathe():
pattern = LEDPattern.solid(Color.WHITE).breathe(units.microseconds(4))
buffer = AddressableLEDBuffer(1)
RobotController.setTimeSource(lambda: 0)
pattern.applyTo(buffer)
assert buffer.getLED8Bit(0) == Color8Bit(Color.WHITE)
RobotController.setTimeSource(lambda: 1)
pattern.applyTo(buffer)
assert buffer.getLED8Bit(0) == Color8Bit(Color(0.5, 0.5, 0.5))
RobotController.setTimeSource(lambda: 2)
pattern.applyTo(buffer)
assert buffer.getLED8Bit(0) == Color8Bit(Color.BLACK)
def test_overlay_solid_on_solid():
overlay = LEDPattern.solid(Color.YELLOW).overlayOn(LEDPattern.solid(Color.WHITE))
buffer = AddressableLEDBuffer(1)
overlay.applyTo(buffer)
assert buffer.getLED8Bit(0) == Color8Bit(Color.YELLOW)
def test_progress_mask_layer():
progress = {"value": 0.0}
pattern = LEDPattern.progressMaskLayer(lambda: progress["value"])
buffer = AddressableLEDBuffer(10)
progress["value"] = 0.3
pattern.applyTo(buffer)
for i in range(3):
assert buffer.getLED8Bit(i) == Color8Bit(Color.WHITE)
for i in range(3, 10):
assert buffer.getLED8Bit(i) == Color8Bit(Color.BLACK)
def test_blend():
pattern = LEDPattern.solid(Color.BLUE).blend(LEDPattern.solid(Color.RED))
buffer = AddressableLEDBuffer(1)
pattern.applyTo(buffer)
assert buffer.getLED8Bit(0) == Color8Bit(Color(127, 0, 127))
def test_binary_mask():
base = LEDPattern.solid(Color(123, 123, 123))
mask = LEDPattern.steps([(0.0, Color.WHITE), (0.5, Color.BLACK)])
pattern = base.mask(mask)
buffer = AddressableLEDBuffer(4)
pattern.applyTo(buffer)
for i in range(2):
assert buffer.getLED8Bit(i) == Color8Bit(Color(123, 123, 123))
for i in range(2, 4):
assert buffer.getLED8Bit(i) == Color8Bit(Color.BLACK)

View File

@@ -1,6 +1,7 @@
from wpilib import OnboardIMU from wpilib import OnboardIMU
from wpilib.simulation import OnboardIMUSim from wpilib.simulation import OnboardIMUSim
def test_sim_device() -> None: def test_sim_device() -> None:
imu = OnboardIMU(OnboardIMU.MountOrientation.FLAT) imu = OnboardIMU(OnboardIMU.MountOrientation.FLAT)
@@ -15,15 +16,15 @@ def test_sim_device() -> None:
assert 0.0 == imu.getAccelX() assert 0.0 == imu.getAccelX()
assert 0.0 == imu.getAccelY() assert 0.0 == imu.getAccelY()
assert 0.0 == imu.getAccelZ() assert 0.0 == imu.getAccelZ()
sim.setAngleX(1) sim.setAngleX(1)
sim.setAngleY(2) sim.setAngleY(2)
sim.setAngleZ(3) sim.setAngleZ(3)
sim.setGyroRateX(3.504) sim.setGyroRateX(3.504)
sim.setGyroRateY(1.91) sim.setGyroRateY(1.91)
sim.setGyroRateZ(22.9) sim.setGyroRateZ(22.9)
sim.setAccelX(-1) sim.setAccelX(-1)
sim.setAccelY(-2) sim.setAccelY(-2)
sim.setAccelZ(-3) sim.setAccelZ(-3)
@@ -38,4 +39,4 @@ def test_sim_device() -> None:
assert -1.0 == imu.getAccelX() assert -1.0 == imu.getAccelX()
assert -2.0 == imu.getAccelY() assert -2.0 == imu.getAccelY()
assert -3.0 == imu.getAccelZ() assert -3.0 == imu.getAccelZ()

View File

@@ -1,8 +1,8 @@
import wpiutil import wpiutil
import time import time
import pytest import pytest
def test_default(): def test_default():
wpi_now = wpiutil.now() * 1e-6 wpi_now = wpiutil.now() * 1e-6
py_now = int(time.time()) py_now = int(time.time())
@@ -14,6 +14,7 @@ def test_default():
NOW_TIMESTAMP_S = 0 NOW_TIMESTAMP_S = 0
def custom_now_getter(): def custom_now_getter():
global NOW_TIMESTAMP_S global NOW_TIMESTAMP_S
return int(NOW_TIMESTAMP_S * 1e6) return int(NOW_TIMESTAMP_S * 1e6)
@@ -33,7 +34,7 @@ def test_custom_timestamp(custom_fixture):
NOW_TIMESTAMP_S = 1.5 NOW_TIMESTAMP_S = 1.5
assert 1_500_000 == wpiutil.now() assert 1_500_000 == wpiutil.now()
NOW_TIMESTAMP_S = 100 NOW_TIMESTAMP_S = 100
assert 100_000_000 == wpiutil.now() assert 100_000_000 == wpiutil.now()
@@ -42,5 +43,3 @@ def test_custom_timestamp(custom_fixture):
wpi_now = wpiutil.now() * 1e-6 wpi_now = wpiutil.now() * 1e-6
py_now = int(time.time()) py_now = int(time.time())
assert py_now == pytest.approx(wpi_now, abs=1) assert py_now == pytest.approx(wpi_now, abs=1)