From bdc93917387d4f415932a3e79897fd42ea432a9d Mon Sep 17 00:00:00 2001 From: PJ Reiniger Date: Wed, 31 Dec 2025 12:05:00 -0500 Subject: [PATCH] [py] Fix opmodes (#8498) Co-authored-by: David Vo --- hal/robotpy_pybind_build_info.bzl | 1 + .../main/python/semiwrap/DriverStation.yml | 2 - .../python/semiwrap/DriverStationTypes.yml | 6 +- .../semiwrap/simulation/DriverStationData.yml | 2 - .../python/semiwrap/simulation/MockHooks.yml | 2 - shared/bazel/rules/robotpy/pytest_util.bzl | 4 +- wpilibc/BUILD.bazel | 1 + wpilibc/robotpy_pybind_build_info.bzl | 1 + .../src/main/python/semiwrap/OpModeRobot.yml | 5 - .../semiwrap/simulation/DriverStationSim.yml | 25 ++- wpilibc/src/main/python/wpilib/__init__.py | 4 +- .../main/python/wpilib/simulation/__init__.py | 6 + wpilibc/src/test/python/test_opmode_robot.py | 156 ++++++++++++++++++ 13 files changed, 196 insertions(+), 19 deletions(-) create mode 100644 wpilibc/src/test/python/test_opmode_robot.py diff --git a/hal/robotpy_pybind_build_info.bzl b/hal/robotpy_pybind_build_info.bzl index f1fffa6f44..4c784538fd 100644 --- a/hal/robotpy_pybind_build_info.bzl +++ b/hal/robotpy_pybind_build_info.bzl @@ -333,6 +333,7 @@ def wpihal_extension(srcs = [], header_to_dat_deps = [], extra_hdrs = [], includ header_file = "$(execpath :robotpy-native-wpihal.copy_headers)/wpi/hal/DriverStationTypes.h", tmpl_class_names = [], trampolines = [ + ("HAL_ControlWord", "__HAL_ControlWord.hpp"), ("HAL_JoystickAxes", "__HAL_JoystickAxes.hpp"), ("HAL_JoystickPOVs", "__HAL_JoystickPOVs.hpp"), ("HAL_JoystickButtons", "__HAL_JoystickButtons.hpp"), diff --git a/hal/src/main/python/semiwrap/DriverStation.yml b/hal/src/main/python/semiwrap/DriverStation.yml index b4338414e0..354a15f7f2 100644 --- a/hal/src/main/python/semiwrap/DriverStation.yml +++ b/hal/src/main/python/semiwrap/DriverStation.yml @@ -42,6 +42,4 @@ functions: HAL_SetOpModeOptions: HAL_ObserveUserProgram: GetControlWord: - ignore: true GetUncachedControlWord: - ignore: true diff --git a/hal/src/main/python/semiwrap/DriverStationTypes.yml b/hal/src/main/python/semiwrap/DriverStationTypes.yml index 72ef21b68e..f2ae04a06c 100644 --- a/hal/src/main/python/semiwrap/DriverStationTypes.yml +++ b/hal/src/main/python/semiwrap/DriverStationTypes.yml @@ -12,11 +12,13 @@ enums: HAL_MatchType: value_prefix: HAL_kMatchType HAL_RobotMode: - ignore: true + rename: _RobotMode RobotMode: classes: HAL_ControlWord: - ignore: true + rename: _ControlWord + attributes: + value: HAL_JoystickAxes: attributes: axes: diff --git a/hal/src/main/python/semiwrap/simulation/DriverStationData.yml b/hal/src/main/python/semiwrap/simulation/DriverStationData.yml index 7b411287df..8cfeb8e4ff 100644 --- a/hal/src/main/python/semiwrap/simulation/DriverStationData.yml +++ b/hal/src/main/python/semiwrap/simulation/DriverStationData.yml @@ -127,6 +127,4 @@ functions: ignore: true HALSIM_CancelOpModeOptionsCallback: HALSIM_GetOpModeOptions: - ignore: true HALSIM_FreeOpModeOptionsArray: - ignore: true diff --git a/hal/src/main/python/semiwrap/simulation/MockHooks.yml b/hal/src/main/python/semiwrap/simulation/MockHooks.yml index 4fe91009fb..94aafdcf71 100644 --- a/hal/src/main/python/semiwrap/simulation/MockHooks.yml +++ b/hal/src/main/python/semiwrap/simulation/MockHooks.yml @@ -11,9 +11,7 @@ functions: HALSIM_SetProgramStarted: HALSIM_GetProgramStarted: HALSIM_SetProgramState: - ignore: true HALSIM_GetProgramState: - ignore: true SetProgramState: GetProgramState: HALSIM_RestartTiming: diff --git a/shared/bazel/rules/robotpy/pytest_util.bzl b/shared/bazel/rules/robotpy/pytest_util.bzl index 322396217e..0cb1a37013 100644 --- a/shared/bazel/rules/robotpy/pytest_util.bzl +++ b/shared/bazel/rules/robotpy/pytest_util.bzl @@ -1,13 +1,13 @@ load("@rules_python_pytest//python_pytest:defs.bzl", "py_pytest_test") load("//shared/bazel/rules/robotpy:compatibility_select.bzl", "robotpy_compatibility_select") -def robotpy_py_test(name, srcs, **kwargs): +def robotpy_py_test(name, srcs, tags = [], **kwargs): py_pytest_test( name = name, size = "small", srcs = srcs, target_compatible_with = robotpy_compatibility_select(), - tags = [ + tags = tags + [ "no-asan", "no-tsan", "robotpy", diff --git a/wpilibc/BUILD.bazel b/wpilibc/BUILD.bazel index 0419e8803f..45a59e4e3b 100644 --- a/wpilibc/BUILD.bazel +++ b/wpilibc/BUILD.bazel @@ -291,6 +291,7 @@ define_pybind_library( robotpy_py_test( "python_tests", srcs = glob(["src/test/python/**/*.py"]), + tags = ["exclusive"], deps = [ ":robotpy-wpilib", requirement("pytest"), diff --git a/wpilibc/robotpy_pybind_build_info.bzl b/wpilibc/robotpy_pybind_build_info.bzl index ea3f672d70..aa7ebf9e34 100644 --- a/wpilibc/robotpy_pybind_build_info.bzl +++ b/wpilibc/robotpy_pybind_build_info.bzl @@ -1513,6 +1513,7 @@ def wpilib_simulation_extension(srcs = [], header_to_dat_deps = [], extra_hdrs = header_file = "$(execpath :robotpy-native-wpilib.copy_headers)/wpi/simulation/DriverStationSim.hpp", tmpl_class_names = [], trampolines = [ + ("wpi::sim::OpModeOptions", "wpi__sim__OpModeOptions.hpp"), ("wpi::sim::DriverStationSim", "wpi__sim__DriverStationSim.hpp"), ], ), diff --git a/wpilibc/src/main/python/semiwrap/OpModeRobot.yml b/wpilibc/src/main/python/semiwrap/OpModeRobot.yml index 3ff5c48da2..9222607e32 100644 --- a/wpilibc/src/main/python/semiwrap/OpModeRobot.yml +++ b/wpilibc/src/main/python/semiwrap/OpModeRobot.yml @@ -19,8 +19,3 @@ classes: ClearOpModes: wpi::OpModeRobot: ignore: true - methods: - AddOpMode: - overloads: - RobotMode, std::string_view, std::string_view, std::string_view, const wpi::util::Color&, const wpi::util::Color&: - RobotMode, std::string_view, std::string_view, std::string_view: diff --git a/wpilibc/src/main/python/semiwrap/simulation/DriverStationSim.yml b/wpilibc/src/main/python/semiwrap/simulation/DriverStationSim.yml index 27778c29ef..515300e81a 100644 --- a/wpilibc/src/main/python/semiwrap/simulation/DriverStationSim.yml +++ b/wpilibc/src/main/python/semiwrap/simulation/DriverStationSim.yml @@ -1,6 +1,27 @@ classes: wpi::sim::OpModeOptions: - ignore: true + ignored_bases: + - std::span + force_no_trampoline: true + methods: + OpModeOptions: + overloads: + "": + ignore: true + HAL_OpModeOption*, int32_t: + ignore: true + inline_code: | + .def("__len__", [](const OpModeOptions &self) { return self.size(); }) + .def("__getitem__", [](const OpModeOptions &self, int index) { + if (index >= static_cast(self.size())) { + throw std::out_of_range("OpModeOptions index out of range"); + } + return self[index]; + }) + .def("__iter__", [](OpModeOptions &self) { + return py::make_iterator(self.begin(), self.end()); + }, py::keep_alive<0,1>()); + wpi::sim::DriverStationSim: force_type_casters: - std::function @@ -54,5 +75,5 @@ classes: GetOpMode: SetOpMode: RegisterOpModeOptionsCallback: - GetOpModeOptions: ignore: true + GetOpModeOptions: diff --git a/wpilibc/src/main/python/wpilib/__init__.py b/wpilibc/src/main/python/wpilib/__init__.py index e5d2d8e525..cbaece8d9e 100644 --- a/wpilibc/src/main/python/wpilib/__init__.py +++ b/wpilibc/src/main/python/wpilib/__init__.py @@ -47,7 +47,6 @@ from ._wpilib import ( OnboardIMU, OpMode, OpModeRobotBase, - PeriodicOpMode, PS4Controller, PS5Controller, PWM, @@ -58,6 +57,7 @@ from ._wpilib import ( PWMTalonSRX, PWMVenom, PWMVictorSPX, + PeriodicOpMode, PneumaticHub, PneumaticsBase, PneumaticsControlModule, @@ -139,7 +139,6 @@ __all__ = [ "OnboardIMU", "OpMode", "OpModeRobotBase", - "PeriodicOpMode", "PS4Controller", "PS5Controller", "PWM", @@ -150,6 +149,7 @@ __all__ = [ "PWMTalonSRX", "PWMVenom", "PWMVictorSPX", + "PeriodicOpMode", "PneumaticHub", "PneumaticsBase", "PneumaticsControlModule", diff --git a/wpilibc/src/main/python/wpilib/simulation/__init__.py b/wpilibc/src/main/python/wpilib/simulation/__init__.py index 041923862f..cc9481b7b6 100644 --- a/wpilibc/src/main/python/wpilib/simulation/__init__.py +++ b/wpilibc/src/main/python/wpilib/simulation/__init__.py @@ -32,6 +32,7 @@ from ._simulation import ( LinearSystemSim_2_1_2, LinearSystemSim_2_2_1, LinearSystemSim_2_2_2, + OpModeOptions, PS4ControllerSim, PS5ControllerSim, PWMMotorControllerSim, @@ -48,11 +49,13 @@ from ._simulation import ( StadiaControllerSim, XboxControllerSim, getProgramStarted, + getProgramState, isTimingPaused, pauseTiming, restartTiming, resumeTiming, setProgramStarted, + setProgramState, setRuntimeType, stepTiming, stepTimingAsync, @@ -87,6 +90,7 @@ __all__ = [ "LinearSystemSim_2_1_2", "LinearSystemSim_2_2_1", "LinearSystemSim_2_2_2", + "OpModeOptions", "PS4ControllerSim", "PS5ControllerSim", "PWMMotorControllerSim", @@ -103,11 +107,13 @@ __all__ = [ "StadiaControllerSim", "XboxControllerSim", "getProgramStarted", + "getProgramState", "isTimingPaused", "pauseTiming", "restartTiming", "resumeTiming", "setProgramStarted", + "setProgramState", "setRuntimeType", "stepTiming", "stepTimingAsync", diff --git a/wpilibc/src/test/python/test_opmode_robot.py b/wpilibc/src/test/python/test_opmode_robot.py new file mode 100644 index 0000000000..7aa9e030d1 --- /dev/null +++ b/wpilibc/src/test/python/test_opmode_robot.py @@ -0,0 +1,156 @@ +import pytest +import threading +from wpilib import simulation as wsim +from wpimath.units import seconds +from wpilib.opmoderobot import OpModeRobot +from wpilib import OpMode +from hal._wpiHal import RobotMode +from wpiutil import Color + + +class MockOpMode(OpMode): + def __init__(self): + super().__init__() + self.disabled_periodic_count = 0 + self.op_mode_run_count = 0 + self.op_mode_stop_count = 0 + + def disabled_periodic(self): + self.disabled_periodic_count += 1 + + def op_mode_run(self, op_mode_id: int): + self.op_mode_run_count += 1 + + def op_mode_stop(self): + self.op_mode_stop_count += 1 + + +class OneArgOpMode(OpMode): + def __init__(self, robot): + super().__init__() + + def op_mode_run(self, op_mode_id: int): + pass + + def op_mode_stop(self): + pass + + +class MockRobot(OpModeRobot): + def __init__(self): + super().__init__() + self.driver_station_connected_count = 0 + self.none_periodic_count = 0 + + def driverStationConnected(self): + self.driver_station_connected_count += 1 + + def nonePeriodic(self): + self.none_periodic_count += 1 + + +@pytest.fixture(autouse=True) +def sim_timing_setup(): + wsim.pauseTiming() + wsim.setProgramStarted(False) + yield + wsim.resumeTiming() + + +def test_add_op_mode(): + class MyMockRobot(MockRobot): + def __init__(self): + super().__init__() + self.addOpMode( + MockOpMode, + RobotMode.AUTONOMOUS, + "NoArgOpMode-Auto", + "Group", + "Description", + Color.kWhite, + Color.kBlack, + ) + self.addOpMode( + OneArgOpMode, + RobotMode.TEST, + "OneArgOpMode-Test", + "Group", + "Description", + Color.kWhite, + Color.kBlack, + ) + self.addOpMode(MockOpMode, RobotMode.TELEOPERATED, "NoArgOpMode") + self.addOpMode(OneArgOpMode, RobotMode.TELEOPERATED, "OneArgOpMode") + self.publishOpModes() + + robot = MyMockRobot() + options = wsim.DriverStationSim.getOpModeOptions() + + assert len(options) == 4 + + opt_map = {opt.name: opt for opt in options} + + auto_opt = opt_map["NoArgOpMode-Auto"] + assert auto_opt.group == "Group" + assert auto_opt.description == "Description" + assert auto_opt.textColor == 0xFFFFFF + assert auto_opt.backgroundColor == 0x000000 + + tele_opt = opt_map["NoArgOpMode"] + assert tele_opt.group == "" + assert tele_opt.description == "" + assert tele_opt.textColor == -1 + assert tele_opt.backgroundColor == -1 + + +def test_clear_op_modes(): + class MyMockRobot(MockRobot): + def __init__(self): + super().__init__() + self.addOpMode(MockOpMode, RobotMode.TELEOPERATED, "NoArgOpMode") + self.publishOpModes() + + robot = MyMockRobot() + robot.clearOpModes() + + options = wsim.DriverStationSim.getOpModeOptions() + assert len(options) == 0 + + +def test_remove_op_mode(): + class MyMockRobot(MockRobot): + def __init__(self): + super().__init__() + self.addOpMode(MockOpMode, RobotMode.TELEOPERATED, "NoArgOpMode") + self.addOpMode(OneArgOpMode, RobotMode.TELEOPERATED, "OneArgOpMode") + self.publishOpModes() + + robot = MyMockRobot() + robot.removeOpMode(RobotMode.TELEOPERATED, "NoArgOpMode") + robot.publishOpModes() + + options = wsim.DriverStationSim.getOpModeOptions() + assert len(options) == 1 + assert options[0].name == "OneArgOpMode" + + +def test_none_periodic(): + class MyMockRobot(MockRobot): + def __init__(self): + super().__init__() + self.addOpMode(MockOpMode, RobotMode.TELEOPERATED, "NoArgOpMode") + self.publishOpModes() + + robot = MyMockRobot() + + robot_thread = threading.Thread(target=robot.startCompetition) + robot_thread.start() + + wsim.waitForProgramStart() + + wsim.stepTiming(0.110) + + assert robot.none_periodic_count == 2 + + robot.endCompetition() + robot_thread.join()