mirror of
https://github.com/wpilibsuite/allwpilib
synced 2026-06-19 00:41:43 +00:00
[copybara] Sync with robotpy (#8964)
GitOrigin-RevId: 9dff8f977401e78be0bb6f39cea2328320ab2d95
This commit is contained in:
@@ -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
|
||||
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
|
||||
"""
|
||||
...
|
||||
@@ -48,7 +54,7 @@ class WaitUntilCommand(Command):
|
||||
self._condition = condition
|
||||
|
||||
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)
|
||||
|
||||
|
||||
@@ -50,6 +50,7 @@ scan_headers_ignore = [
|
||||
|
||||
# TODO: might want this in the future
|
||||
"mrc/*",
|
||||
"mrclib/*",
|
||||
|
||||
"src/ds_types_fmt.h",
|
||||
"sim_cb.h",
|
||||
|
||||
@@ -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!
|
||||
@@ -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()
|
||||
@@ -18,7 +18,7 @@ class MyRobot(wpilib.TimedRobot):
|
||||
self.leftDrive = wpilib.PWMSparkMax(0)
|
||||
self.rightDrive = wpilib.PWMSparkMax(1)
|
||||
self.robotDrive = wpilib.DifferentialDrive(self.leftDrive, self.rightDrive)
|
||||
self.controller = wpilib.NiDsXboxController(0)
|
||||
self.controller = wpilib.Gamepad(0)
|
||||
self.timer = wpilib.Timer()
|
||||
|
||||
# We need to invert one side of the drivetrain so that positive voltages
|
||||
|
||||
@@ -18,7 +18,7 @@ class MyRobot(wpilib.TimedRobot):
|
||||
|
||||
# Reuse buffer
|
||||
# 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))
|
||||
|
||||
# Set the data
|
||||
|
||||
@@ -234,6 +234,7 @@ PKG_CONFIG_DEPS = [
|
||||
generate_robotpy_pybind_build_info(
|
||||
name = "robotpy-wpilib-generator",
|
||||
additional_srcs = [
|
||||
"src/main/python/wpilib/src/rpy/AddressableLEDBuffer.h",
|
||||
"src/main/python/wpilib/src/rpy/Filesystem.h",
|
||||
"src/main/python/wpilib/src/rpy/Notifier.h",
|
||||
"src/main/python/wpilib/src/rpy/MotorControllerGroup.h",
|
||||
|
||||
11
wpilibc/robotpy_pybind_build_info.bzl
generated
11
wpilibc/robotpy_pybind_build_info.bzl
generated
@@ -36,6 +36,17 @@ def wpilib_extension(srcs = [], header_to_dat_deps = [], extra_hdrs = [], includ
|
||||
("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(
|
||||
class_name = "EdgeConfiguration",
|
||||
yml_file = "semiwrap/EdgeConfiguration.yml",
|
||||
|
||||
@@ -94,6 +94,8 @@ DYNAMIC_CAMERA_SERVER = 1
|
||||
Filesystem = "rpy/Filesystem.h"
|
||||
MotorControllerGroup = "rpy/MotorControllerGroup.h"
|
||||
Notifier = "rpy/Notifier.h"
|
||||
# rpy only
|
||||
AddressableLEDBuffer = "rpy/AddressableLEDBuffer.h"
|
||||
|
||||
# wpi/counter
|
||||
EdgeConfiguration = "wpi/counter/EdgeConfiguration.hpp"
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
extra_includes:
|
||||
- rpy/AddressableLEDBuffer.h
|
||||
|
||||
functions:
|
||||
format_as:
|
||||
ignore: true
|
||||
@@ -19,6 +22,10 @@ classes:
|
||||
SetGlobalData:
|
||||
enums:
|
||||
ColorOrder:
|
||||
inline_code: |
|
||||
.def("setData", [](wpi::AddressableLED& self, const wpi::AddressableLEDBuffer& data) {
|
||||
return self.SetData(data);
|
||||
}, release_gil(), py::prepend());
|
||||
wpi::AddressableLED::LEDData:
|
||||
force_no_trampoline: true
|
||||
ignored_bases:
|
||||
|
||||
58
wpilibc/src/main/python/semiwrap/AddressableLEDBuffer.yml
Normal file
58
wpilibc/src/main/python/semiwrap/AddressableLEDBuffer.yml
Normal 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>())
|
||||
@@ -1,3 +1,6 @@
|
||||
extra_includes:
|
||||
- rpy/AddressableLEDBuffer.h
|
||||
|
||||
classes:
|
||||
wpi::LEDPattern:
|
||||
enums:
|
||||
@@ -8,8 +11,16 @@ classes:
|
||||
ApplyTo:
|
||||
overloads:
|
||||
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]:
|
||||
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:
|
||||
OffsetBy:
|
||||
ScrollAtRelativeVelocity:
|
||||
@@ -38,6 +49,14 @@ classes:
|
||||
ignore: true
|
||||
Rainbow:
|
||||
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:
|
||||
methods:
|
||||
LEDReader:
|
||||
|
||||
@@ -4,6 +4,7 @@ from . import _init__wpilib
|
||||
from ._wpilib import (
|
||||
ADXL345_I2C,
|
||||
AddressableLED,
|
||||
AddressableLEDBuffer,
|
||||
Alert,
|
||||
Alliance,
|
||||
AnalogAccelerometer,
|
||||
@@ -117,6 +118,7 @@ from ._wpilib import (
|
||||
__all__ = [
|
||||
"ADXL345_I2C",
|
||||
"AddressableLED",
|
||||
"AddressableLEDBuffer",
|
||||
"Alert",
|
||||
"Alliance",
|
||||
"AnalogAccelerometer",
|
||||
|
||||
@@ -177,7 +177,7 @@ class RobotStarter:
|
||||
for i in range(100):
|
||||
if (
|
||||
inst.getNetworkMode()
|
||||
& ntcore.NetworkTableInstance.NetworkMode.kNetModeStarting
|
||||
& ntcore.NetworkTableInstance.NetworkMode.STARTING.value
|
||||
) == 0:
|
||||
break
|
||||
# real sleep since we're waiting for the server, not simulated sleep
|
||||
|
||||
129
wpilibc/src/main/python/wpilib/src/rpy/AddressableLEDBuffer.cpp
Normal file
129
wpilibc/src/main/python/wpilib/src/rpy/AddressableLEDBuffer.cpp
Normal 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
|
||||
285
wpilibc/src/main/python/wpilib/src/rpy/AddressableLEDBuffer.h
Normal file
285
wpilibc/src/main/python/wpilib/src/rpy/AddressableLEDBuffer.h
Normal 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
|
||||
164
wpilibc/src/test/python/test_addressable_led_buffer.py
Normal file
164
wpilibc/src/test/python/test_addressable_led_buffer.py
Normal 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
|
||||
329
wpilibc/src/test/python/test_led_pattern.py
Normal file
329
wpilibc/src/test/python/test_led_pattern.py
Normal 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)
|
||||
@@ -1,6 +1,7 @@
|
||||
from wpilib import OnboardIMU
|
||||
from wpilib.simulation import OnboardIMUSim
|
||||
|
||||
|
||||
def test_sim_device() -> None:
|
||||
|
||||
imu = OnboardIMU(OnboardIMU.MountOrientation.FLAT)
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
|
||||
import wpiutil
|
||||
import time
|
||||
import pytest
|
||||
|
||||
|
||||
def test_default():
|
||||
wpi_now = wpiutil.now() * 1e-6
|
||||
py_now = int(time.time())
|
||||
@@ -14,6 +14,7 @@ def test_default():
|
||||
|
||||
NOW_TIMESTAMP_S = 0
|
||||
|
||||
|
||||
def custom_now_getter():
|
||||
global NOW_TIMESTAMP_S
|
||||
return int(NOW_TIMESTAMP_S * 1e6)
|
||||
@@ -42,5 +43,3 @@ def test_custom_timestamp(custom_fixture):
|
||||
wpi_now = wpiutil.now() * 1e-6
|
||||
py_now = int(time.time())
|
||||
assert py_now == pytest.approx(wpi_now, abs=1)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user