From 111130d8bb0f88c76cc44cfa601d6a04bd24e30c Mon Sep 17 00:00:00 2001 From: PJ Reiniger Date: Mon, 8 Jun 2026 22:22:48 -0400 Subject: [PATCH] [copybara] Sync with robotpy (#8964) GitOrigin-RevId: 9dff8f977401e78be0bb6f39cea2328320ab2d95 --- .../main/python/commands2/waituntilcommand.py | 8 +- hal/src/main/python/pyproject.toml | 1 + robotpyExamples/CONTRIBUTING.md | 174 --------- robotpyExamples/check_header.py | 79 ----- .../examples/GettingStarted/robot.py | 2 +- .../snippets/AddressableLED/robot.py | 2 +- wpilibc/BUILD.bazel | 1 + wpilibc/robotpy_pybind_build_info.bzl | 11 + wpilibc/src/main/python/pyproject.toml | 2 + .../main/python/semiwrap/AddressableLED.yml | 7 + .../python/semiwrap/AddressableLEDBuffer.yml | 58 +++ .../src/main/python/semiwrap/LEDPattern.yml | 19 + wpilibc/src/main/python/wpilib/__init__.py | 2 + wpilibc/src/main/python/wpilib/_impl/start.py | 2 +- .../wpilib/src/rpy/AddressableLEDBuffer.cpp | 129 +++++++ .../wpilib/src/rpy/AddressableLEDBuffer.h | 285 +++++++++++++++ .../python/test_addressable_led_buffer.py | 164 +++++++++ wpilibc/src/test/python/test_led_pattern.py | 329 ++++++++++++++++++ wpilibc/src/test/python/test_onboard_imu.py | 9 +- wpiutil/src/test/python/test_timestamp.py | 7 +- 20 files changed, 1026 insertions(+), 265 deletions(-) delete mode 100644 robotpyExamples/CONTRIBUTING.md delete mode 100755 robotpyExamples/check_header.py create mode 100644 wpilibc/src/main/python/semiwrap/AddressableLEDBuffer.yml create mode 100644 wpilibc/src/main/python/wpilib/src/rpy/AddressableLEDBuffer.cpp create mode 100644 wpilibc/src/main/python/wpilib/src/rpy/AddressableLEDBuffer.h create mode 100644 wpilibc/src/test/python/test_addressable_led_buffer.py create mode 100644 wpilibc/src/test/python/test_led_pattern.py diff --git a/commandsv2/src/main/python/commands2/waituntilcommand.py b/commandsv2/src/main/python/commands2/waituntilcommand.py index c195a550f7..53424585c5 100644 --- a/commandsv2/src/main/python/commands2/waituntilcommand.py +++ b/commandsv2/src/main/python/commands2/waituntilcommand.py @@ -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) diff --git a/hal/src/main/python/pyproject.toml b/hal/src/main/python/pyproject.toml index 40ca2664dd..f01b8676f7 100644 --- a/hal/src/main/python/pyproject.toml +++ b/hal/src/main/python/pyproject.toml @@ -50,6 +50,7 @@ scan_headers_ignore = [ # TODO: might want this in the future "mrc/*", + "mrclib/*", "src/ds_types_fmt.h", "sim_cb.h", diff --git a/robotpyExamples/CONTRIBUTING.md b/robotpyExamples/CONTRIBUTING.md deleted file mode 100644 index a79b7dbbe0..0000000000 --- a/robotpyExamples/CONTRIBUTING.md +++ /dev/null @@ -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! diff --git a/robotpyExamples/check_header.py b/robotpyExamples/check_header.py deleted file mode 100755 index 66e39e34b7..0000000000 --- a/robotpyExamples/check_header.py +++ /dev/null @@ -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() diff --git a/robotpyExamples/examples/GettingStarted/robot.py b/robotpyExamples/examples/GettingStarted/robot.py index 1b624d15aa..289b0c2ad0 100755 --- a/robotpyExamples/examples/GettingStarted/robot.py +++ b/robotpyExamples/examples/GettingStarted/robot.py @@ -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 diff --git a/robotpyExamples/snippets/AddressableLED/robot.py b/robotpyExamples/snippets/AddressableLED/robot.py index 6371cebff0..5d4d7e906d 100644 --- a/robotpyExamples/snippets/AddressableLED/robot.py +++ b/robotpyExamples/snippets/AddressableLED/robot.py @@ -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 diff --git a/wpilibc/BUILD.bazel b/wpilibc/BUILD.bazel index 781c754535..a7a536c807 100644 --- a/wpilibc/BUILD.bazel +++ b/wpilibc/BUILD.bazel @@ -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", diff --git a/wpilibc/robotpy_pybind_build_info.bzl b/wpilibc/robotpy_pybind_build_info.bzl index c298998eef..dc55d0073e 100644 --- a/wpilibc/robotpy_pybind_build_info.bzl +++ b/wpilibc/robotpy_pybind_build_info.bzl @@ -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", diff --git a/wpilibc/src/main/python/pyproject.toml b/wpilibc/src/main/python/pyproject.toml index c849304ddc..ec02f66299 100644 --- a/wpilibc/src/main/python/pyproject.toml +++ b/wpilibc/src/main/python/pyproject.toml @@ -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" diff --git a/wpilibc/src/main/python/semiwrap/AddressableLED.yml b/wpilibc/src/main/python/semiwrap/AddressableLED.yml index 3643f333a5..d759d42975 100644 --- a/wpilibc/src/main/python/semiwrap/AddressableLED.yml +++ b/wpilibc/src/main/python/semiwrap/AddressableLED.yml @@ -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: diff --git a/wpilibc/src/main/python/semiwrap/AddressableLEDBuffer.yml b/wpilibc/src/main/python/semiwrap/AddressableLEDBuffer.yml new file mode 100644 index 0000000000..a07ea69452 --- /dev/null +++ b/wpilibc/src/main/python/semiwrap/AddressableLEDBuffer.yml @@ -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>()) diff --git a/wpilibc/src/main/python/semiwrap/LEDPattern.yml b/wpilibc/src/main/python/semiwrap/LEDPattern.yml index 6ae8f332b4..8824666657 100644 --- a/wpilibc/src/main/python/semiwrap/LEDPattern.yml +++ b/wpilibc/src/main/python/semiwrap/LEDPattern.yml @@ -1,3 +1,6 @@ +extra_includes: +- rpy/AddressableLEDBuffer.h + classes: wpi::LEDPattern: enums: @@ -8,8 +11,16 @@ classes: ApplyTo: overloads: std::span [const]: + cpp_code: | + [](const wpi::LEDPattern& self, wpi::AddressableLEDBuffer::View data) { + return self.ApplyTo(data); + } LEDReader, std::function [const]: std::span, std::function [const]: + cpp_code: | + [](const wpi::LEDPattern& self, wpi::AddressableLEDBuffer::View data, std::function 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>(data)); + }, py::arg("data"), release_gil()) + .def("applyTo", [](const wpi::LEDPattern& self, wpi::AddressableLEDBuffer& data, + std::function writer) { + self.ApplyTo(static_cast>(data), std::move(writer)); + }, py::arg("data"), py::arg("writer").none(false), release_gil()) wpi::LEDPattern::LEDReader: methods: LEDReader: diff --git a/wpilibc/src/main/python/wpilib/__init__.py b/wpilibc/src/main/python/wpilib/__init__.py index da8a19cef8..d2d9e77420 100644 --- a/wpilibc/src/main/python/wpilib/__init__.py +++ b/wpilibc/src/main/python/wpilib/__init__.py @@ -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", diff --git a/wpilibc/src/main/python/wpilib/_impl/start.py b/wpilibc/src/main/python/wpilib/_impl/start.py index ae0053e287..fdac167875 100644 --- a/wpilibc/src/main/python/wpilib/_impl/start.py +++ b/wpilibc/src/main/python/wpilib/_impl/start.py @@ -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 diff --git a/wpilibc/src/main/python/wpilib/src/rpy/AddressableLEDBuffer.cpp b/wpilibc/src/main/python/wpilib/src/rpy/AddressableLEDBuffer.cpp new file mode 100644 index 0000000000..5e04259a1c --- /dev/null +++ b/wpilibc/src/main/python/wpilib/src/rpy/AddressableLEDBuffer.cpp @@ -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 +#include + +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 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 diff --git a/wpilibc/src/main/python/wpilib/src/rpy/AddressableLEDBuffer.h b/wpilibc/src/main/python/wpilib/src/rpy/AddressableLEDBuffer.h new file mode 100644 index 0000000000..ec99cdda79 --- /dev/null +++ b/wpilibc/src/main/python/wpilib/src/rpy/AddressableLEDBuffer.h @@ -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 +#include +#include +#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() { + return std::span{m_buffer}; + } + + /** + * Implicit conversion to span of const LED data + */ + operator std::span() 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() { + return m_data; + } + + /** + * Implicit conversion to span of const LED data + */ + operator std::span() const { + return m_data; + } + + private: + friend class AddressableLEDBuffer; + explicit View(std::span data); + + std::span 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 m_buffer; +}; + +} // namespace frc diff --git a/wpilibc/src/test/python/test_addressable_led_buffer.py b/wpilibc/src/test/python/test_addressable_led_buffer.py new file mode 100644 index 0000000000..21ac8e681a --- /dev/null +++ b/wpilibc/src/test/python/test_addressable_led_buffer.py @@ -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 diff --git a/wpilibc/src/test/python/test_led_pattern.py b/wpilibc/src/test/python/test_led_pattern.py new file mode 100644 index 0000000000..8e8432da62 --- /dev/null +++ b/wpilibc/src/test/python/test_led_pattern.py @@ -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) diff --git a/wpilibc/src/test/python/test_onboard_imu.py b/wpilibc/src/test/python/test_onboard_imu.py index 382a738c2d..d872e29e17 100644 --- a/wpilibc/src/test/python/test_onboard_imu.py +++ b/wpilibc/src/test/python/test_onboard_imu.py @@ -1,6 +1,7 @@ from wpilib import OnboardIMU from wpilib.simulation import OnboardIMUSim + def test_sim_device() -> None: imu = OnboardIMU(OnboardIMU.MountOrientation.FLAT) @@ -15,15 +16,15 @@ def test_sim_device() -> None: assert 0.0 == imu.getAccelX() assert 0.0 == imu.getAccelY() assert 0.0 == imu.getAccelZ() - + sim.setAngleX(1) sim.setAngleY(2) sim.setAngleZ(3) - + sim.setGyroRateX(3.504) sim.setGyroRateY(1.91) sim.setGyroRateZ(22.9) - + sim.setAccelX(-1) sim.setAccelY(-2) sim.setAccelZ(-3) @@ -38,4 +39,4 @@ def test_sim_device() -> None: assert -1.0 == imu.getAccelX() assert -2.0 == imu.getAccelY() - assert -3.0 == imu.getAccelZ() \ No newline at end of file + assert -3.0 == imu.getAccelZ() diff --git a/wpiutil/src/test/python/test_timestamp.py b/wpiutil/src/test/python/test_timestamp.py index 7bee1ae44e..4126cb4365 100644 --- a/wpiutil/src/test/python/test_timestamp.py +++ b/wpiutil/src/test/python/test_timestamp.py @@ -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) @@ -33,7 +34,7 @@ def test_custom_timestamp(custom_fixture): NOW_TIMESTAMP_S = 1.5 assert 1_500_000 == wpiutil.now() - + NOW_TIMESTAMP_S = 100 assert 100_000_000 == wpiutil.now() @@ -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) - -