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
|
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)
|
||||||
|
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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.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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
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"),
|
("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",
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
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:
|
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:
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
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 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)
|
||||||
|
|||||||
@@ -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)
|
||||||
@@ -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)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user