From 1e2461971dbe308c6d66bb11c1b7c3b3834f1321 Mon Sep 17 00:00:00 2001 From: Dustin Spicuzza Date: Thu, 16 Jul 2026 02:58:49 -0400 Subject: [PATCH 1/2] Add Python opmode decorators and autodiscovery --- .../tests/framework/test_opmode_robot.py | 369 +++++++++++++++++- subprojects/robotpy-wpilib/wpilib/__init__.py | 4 +- .../robotpy-wpilib/wpilib/opmoderobot.py | 305 ++++++++++++++- 3 files changed, 671 insertions(+), 7 deletions(-) diff --git a/subprojects/robotpy-wpilib/tests/framework/test_opmode_robot.py b/subprojects/robotpy-wpilib/tests/framework/test_opmode_robot.py index 3d7046476..2d9ca1996 100644 --- a/subprojects/robotpy-wpilib/tests/framework/test_opmode_robot.py +++ b/subprojects/robotpy-wpilib/tests/framework/test_opmode_robot.py @@ -1,8 +1,9 @@ +import importlib import pytest import threading from wpilib import simulation as wsim -from wpilib.opmoderobot import OpModeRobot -from wpilib import OpMode, RobotState +from wpilib.opmoderobot import OpModeRobot, autonomous, teleop, utility +from wpilib import OpMode, RobotState, autonomous as top_level_autonomous from hal import RobotMode from wpiutil import Color @@ -61,6 +62,68 @@ def sim_timing_setup(): RobotState.clear_opmodes() +def test_opmode_decorators_attach_metadata(): + @autonomous(group="Drive", description="Auto desc") + class AutoMode(OpMode): + pass + + metadata = AutoMode._wpilib_opmode_metadata + assert metadata.mode == RobotMode.AUTONOMOUS + assert metadata.name == "AutoMode" + assert metadata.group == "Drive" + assert metadata.description == "Auto desc" + + +def test_opmode_decorator_rejects_multiple_modes(): + with pytest.raises(ValueError, match="multiple opmode decorators"): + + @teleop + @autonomous + class BadMode(OpMode): + pass + + +def test_opmode_decorator_allows_decorated_subclass(): + @autonomous + class BaseMode(OpMode): + pass + + @teleop + class DerivedMode(BaseMode): + pass + + assert "_wpilib_opmode_metadata" in BaseMode.__dict__ + assert "_wpilib_opmode_metadata" in DerivedMode.__dict__ + assert BaseMode._wpilib_opmode_metadata.mode == RobotMode.AUTONOMOUS + assert DerivedMode._wpilib_opmode_metadata.mode == RobotMode.TELEOPERATED + + +def test_opmode_decorator_rejects_non_opmode_class_eagerly(): + with pytest.raises(TypeError, match="must inherit from OpMode"): + + @autonomous + class NotAnOpMode: + pass + + +def test_opmode_decorator_preserves_explicit_metadata(): + @utility( + name="Arm Test", + group="Mechanisms", + description="tests arm", + text_color=Color.WHITE, + background_color=Color.BLACK, + ) + class UtilityMode(OpMode): + pass + + metadata = UtilityMode._wpilib_opmode_metadata + assert metadata.name == "Arm Test" + assert metadata.text_color == Color.WHITE + assert metadata.background_color == Color.BLACK + assert top_level_autonomous is autonomous + + def test_add_opmode(): class MyMockRobot(MockRobot): def __init__(self): @@ -186,3 +249,305 @@ def test_robot_periodic(periodic_robot_test_fixture): # Additional time steps should continue calling robot_periodic wsim.step_timing(kPeriod) assert robot.periodic_count == 2 + + +def test_opmode_robot_auto_discovers_decorated_modules(tmp_path, monkeypatch): + pkg = tmp_path / "samplebot" + pkg.mkdir() + (pkg / "__init__.py").write_text("") + (pkg / "robot.py").write_text("""\ +from wpilib.opmoderobot import OpModeRobot +class Robot(OpModeRobot): + def __init__(self): + super().__init__() +""") + (pkg / "default_auto_mode.py").write_text("""\ +from wpilib import PeriodicOpMode +from wpilib.opmoderobot import autonomous +@autonomous +class DefaultAutoMode(PeriodicOpMode): + pass +""") + (pkg / "default_tele_mode.py").write_text("""\ +from wpilib import PeriodicOpMode +from wpilib.opmoderobot import teleop +@teleop +class DefaultTeleMode(PeriodicOpMode): + pass +""") + + monkeypatch.syspath_prepend(str(tmp_path)) + module = importlib.import_module("samplebot.robot") + robot = module.Robot() + + options = wsim.DriverStationSim.get_opmode_options() + assert {opt.name for opt in options} == {"DefaultAutoMode", "DefaultTeleMode"} + + +def test_opmode_robot_discovery_bypasses_publish_override(tmp_path, monkeypatch): + pkg = tmp_path / "publishoverridebot" + pkg.mkdir() + (pkg / "__init__.py").write_text("") + (pkg / "robot.py").write_text("""\ +from wpilib.opmoderobot import OpModeRobot +class Robot(OpModeRobot): + def __init__(self): + super().__init__() + self.initialized = True + + def publish_opmodes(self): + assert self.initialized +""") + (pkg / "drive_mode.py").write_text("""\ +from wpilib import PeriodicOpMode +from wpilib.opmoderobot import teleop +@teleop +class DriveMode(PeriodicOpMode): + pass +""") + + monkeypatch.syspath_prepend(str(tmp_path)) + module = importlib.import_module("publishoverridebot.robot") + module.Robot() + + options = wsim.DriverStationSim.get_opmode_options() + assert {opt.name for opt in options} == {"DriveMode"} + + +def test_opmode_robot_ignores_undecorated_subclass_of_decorated_opmode( + tmp_path, monkeypatch +): + pkg = tmp_path / "inheritedmetadatabot" + pkg.mkdir() + (pkg / "__init__.py").write_text("") + (pkg / "robot.py").write_text("""\ +from wpilib.opmoderobot import OpModeRobot +class Robot(OpModeRobot): + def __init__(self): + super().__init__() +""") + (pkg / "drive_modes.py").write_text("""\ +from wpilib import PeriodicOpMode +from wpilib.opmoderobot import teleop +@teleop +class BaseDriveMode(PeriodicOpMode): + pass +class DerivedDriveMode(BaseDriveMode): + pass +""") + + monkeypatch.syspath_prepend(str(tmp_path)) + module = importlib.import_module("inheritedmetadatabot.robot") + module.Robot() + + options = wsim.DriverStationSim.get_opmode_options() + assert {opt.name for opt in options} == {"BaseDriveMode"} + + +def test_opmode_robot_skips_non_candidate_files(tmp_path, monkeypatch): + pkg = tmp_path / "safeimportbot" + pkg.mkdir() + (pkg / "__init__.py").write_text("") + (pkg / "robot.py").write_text("""\ +from wpilib.opmoderobot import OpModeRobot +class Robot(OpModeRobot): + def __init__(self): + super().__init__() +""") + (pkg / "default_auto_mode.py").write_text("""\ +from wpilib import PeriodicOpMode +from wpilib.opmoderobot import autonomous +@autonomous +class DefaultAutoMode(PeriodicOpMode): + pass +""") + (pkg / "helper.py").write_text("raise RuntimeError('should not import')\n") + nested = pkg / "support" + nested.mkdir() + (nested / "bad_auto.py").write_text("""\ +from wpilib import PeriodicOpMode +from wpilib.opmoderobot import autonomous +raise RuntimeError('should not import nested module outside opmodes') +@autonomous +class BadAuto(PeriodicOpMode): + pass +""") + + monkeypatch.syspath_prepend(str(tmp_path)) + module = importlib.import_module("safeimportbot.robot") + module.Robot() + + options = wsim.DriverStationSim.get_opmode_options() + assert {opt.name for opt in options} == {"DefaultAutoMode"} + + +def test_opmode_robot_discovers_opmodes_package_recursively(tmp_path, monkeypatch): + pkg = tmp_path / "nestedopmodesbot" + pkg.mkdir() + (pkg / "__init__.py").write_text("") + (pkg / "robot.py").write_text("""\ +from wpilib.opmoderobot import OpModeRobot +class Robot(OpModeRobot): + def __init__(self): + super().__init__() +""") + opmodes = pkg / "opmodes" + opmodes.mkdir() + (opmodes / "__init__.py").write_text("") + (opmodes / "drive.py").write_text("""\ +from wpilib import PeriodicOpMode +from wpilib.opmoderobot import teleop +@teleop +class DriveMode(PeriodicOpMode): + pass +""") + nested = opmodes / "test" + nested.mkdir() + (nested / "__init__.py").write_text("") + (nested / "servo.py").write_text("""\ +from wpilib import PeriodicOpMode +from wpilib.opmoderobot import utility +@utility +class ServoMode(PeriodicOpMode): + pass +""") + + monkeypatch.syspath_prepend(str(tmp_path)) + module = importlib.import_module("nestedopmodesbot.robot") + module.Robot() + + options = wsim.DriverStationSim.get_opmode_options() + assert {opt.name for opt in options} == {"DriveMode", "ServoMode"} + + +def test_opmode_robot_fails_on_syntax_error_in_scan_tree(tmp_path, monkeypatch): + pkg = tmp_path / "brokenbot" + pkg.mkdir() + (pkg / "__init__.py").write_text("") + (pkg / "robot.py").write_text("""\ +from wpilib.opmoderobot import OpModeRobot +class Robot(OpModeRobot): + def __init__(self): + super().__init__() +""") + (pkg / "broken.py").write_text("def nope(:\n") + + monkeypatch.syspath_prepend(str(tmp_path)) + module = importlib.import_module("brokenbot.robot") + + with pytest.raises(RuntimeError, match="broken.py"): + module.Robot() + + +def test_opmode_robot_fails_on_candidate_import_error(tmp_path, monkeypatch): + pkg = tmp_path / "importbrokenbot" + pkg.mkdir() + (pkg / "__init__.py").write_text("") + (pkg / "robot.py").write_text("""\ +from wpilib.opmoderobot import OpModeRobot +class Robot(OpModeRobot): + def __init__(self): + super().__init__() +""") + (pkg / "bad_auto.py").write_text("""\ +from wpilib import PeriodicOpMode +from wpilib.opmoderobot import autonomous +raise RuntimeError('boom') +@autonomous +class BadAuto(PeriodicOpMode): + pass +""") + + monkeypatch.syspath_prepend(str(tmp_path)) + module = importlib.import_module("importbrokenbot.robot") + + with pytest.raises(RuntimeError, match="bad_auto.py"): + module.Robot() + + +def test_opmode_robot_rejects_duplicate_names_within_mode(tmp_path, monkeypatch): + pkg = tmp_path / "duplicatebot" + pkg.mkdir() + (pkg / "__init__.py").write_text("") + (pkg / "robot.py").write_text("""\ +from wpilib.opmoderobot import OpModeRobot +class Robot(OpModeRobot): + def __init__(self): + super().__init__() +""") + (pkg / "drive_modes.py").write_text("""\ +from wpilib import PeriodicOpMode +from wpilib.opmoderobot import teleop +@teleop(name='Drive') +class DriveModeA(PeriodicOpMode): + pass +@teleop(name='Drive') +class DriveModeB(PeriodicOpMode): + pass +""") + + monkeypatch.syspath_prepend(str(tmp_path)) + module = importlib.import_module("duplicatebot.robot") + + with pytest.raises(ValueError, match="duplicate"): + module.Robot() + + +def test_opmode_robot_rejects_decorated_non_opmode_class(tmp_path, monkeypatch): + pkg = tmp_path / "typecheckbot" + pkg.mkdir() + (pkg / "__init__.py").write_text("") + (pkg / "robot.py").write_text("""\ +from wpilib.opmoderobot import OpModeRobot +class Robot(OpModeRobot): + def __init__(self): + super().__init__() +""") + (pkg / "not_an_opmode.py").write_text("""\ +from wpilib.opmoderobot import autonomous +@autonomous +class NotAnOpMode: + pass +""") + + monkeypatch.syspath_prepend(str(tmp_path)) + module = importlib.import_module("typecheckbot.robot") + + with pytest.raises(RuntimeError, match="OpMode"): + module.Robot() + + +def test_expansion_hub_style_project_discovers_split_opmodes(tmp_path, monkeypatch): + pkg = tmp_path / "expansionhubsample" + pkg.mkdir() + (pkg / "__init__.py").write_text("") + (pkg / "robot.py").write_text("""\ +from wpilib.opmoderobot import OpModeRobot +class Robot(OpModeRobot): + motor0 = None + def __init__(self): + super().__init__() +""") + (pkg / "defaultautomode.py").write_text("""\ +from wpilib import PeriodicOpMode +from wpilib.opmoderobot import autonomous +@autonomous +class DefaultAutoMode(PeriodicOpMode): + def __init__(self, robot): + self.robot = robot +""") + (pkg / "defaulttelemode.py").write_text("""\ +from wpilib import PeriodicOpMode +from wpilib.opmoderobot import teleop +@teleop +class DefaultTeleMode(PeriodicOpMode): + def __init__(self, robot): + self.robot = robot +""") + + monkeypatch.syspath_prepend(str(tmp_path)) + module = importlib.import_module("expansionhubsample.robot") + module.Robot() + + options = wsim.DriverStationSim.get_opmode_options() + assert {opt.name for opt in options} == {"DefaultAutoMode", "DefaultTeleMode"} diff --git a/subprojects/robotpy-wpilib/wpilib/__init__.py b/subprojects/robotpy-wpilib/wpilib/__init__.py index 4be0edb19..e635ed7c0 100644 --- a/subprojects/robotpy-wpilib/wpilib/__init__.py +++ b/subprojects/robotpy-wpilib/wpilib/__init__.py @@ -268,9 +268,9 @@ del _init__wpilib -from .opmoderobot import OpModeRobot +from .opmoderobot import OpModeRobot, autonomous, teleop, utility -__all__ += ["OpModeRobot"] +__all__ += ["OpModeRobot", "autonomous", "teleop", "utility"] from .cameraserver import CameraServer from .deployinfo import get_deploy_data diff --git a/subprojects/robotpy-wpilib/wpilib/opmoderobot.py b/subprojects/robotpy-wpilib/wpilib/opmoderobot.py index 246a11a97..3f5d8bd10 100644 --- a/subprojects/robotpy-wpilib/wpilib/opmoderobot.py +++ b/subprojects/robotpy-wpilib/wpilib/opmoderobot.py @@ -1,10 +1,301 @@ +import ast +import importlib +import inspect +from dataclasses import dataclass from hal import RobotMode -from typing import Optional +from pathlib import Path +from typing import Callable, Optional, TypeVar, overload from wpiutil import Color -__all__ = ["OpModeRobot"] +__all__ = ["OpModeRobot", "autonomous", "teleop", "utility"] -from ._wpilib import OpModeRobotBase, OpMode +from ._wpilib import OpModeRobotBase, OpMode, RobotState + + +@dataclass(frozen=True) +class _OpModeMetadata: + mode: RobotMode + name: str | None + group: str | None + description: str | None + text_color: Color | None + background_color: Color | None + + +_OpModeType = TypeVar("_OpModeType", bound=type[OpMode]) +_DECORATOR_NAMES = {"autonomous", "teleop", "utility"} + + +def _attach_opmode_metadata( + cls: _OpModeType, + *, + mode: RobotMode, + name: str | None = None, + group: str | None = None, + description: str | None = None, + text_color: Color | None = None, + background_color: Color | None = None, +) -> _OpModeType: + if not issubclass(cls, OpMode): + raise TypeError( + f"Decorated class {cls.__module__}.{cls.__qualname__} must inherit from OpMode" + ) + + if "_wpilib_opmode_metadata" in cls.__dict__: + raise ValueError("multiple opmode decorators are not allowed") + + cls._wpilib_opmode_metadata = _OpModeMetadata( + mode=mode, + name=name or cls.__name__, + group=group, + description=description, + text_color=text_color, + background_color=background_color, + ) + return cls + + +def _make_opmode_decorator(mode: RobotMode, _cls=None, **kwargs): + def decorator(cls: _OpModeType) -> _OpModeType: + return _attach_opmode_metadata(cls, mode=mode, **kwargs) + + if _cls is None: + return decorator + return decorator(_cls) + + +@overload +def autonomous(cls: _OpModeType, /) -> _OpModeType: ... + + +@overload +def autonomous( + cls: None = None, + /, + *, + name: str | None = None, + group: str | None = None, + description: str | None = None, + text_color: Color | None = None, + background_color: Color | None = None, +) -> Callable[[_OpModeType], _OpModeType]: ... + + +def autonomous(_cls=None, **kwargs): + """ + Decorator for automatic registration of autonomous opmode classes. + + May be used with or without arguments:: + + @autonomous + class DefaultAutoMode(PeriodicOpMode): + ... + + @autonomous(name="Two Ball", group="Autos", description="Example") + class TwoBallAuto(PeriodicOpMode): + ... + + ``name`` is shown as the selection name in the Driver Station and must be + unique across autonomous opmodes in the project. If omitted, the class name + is used. ``group`` controls Driver Station grouping and defaults to + ungrouped. ``description`` is optional. ``text_color`` and + ``background_color`` are optional display colors; both must be provided to + have an effect. + """ + return _make_opmode_decorator(RobotMode.AUTONOMOUS, _cls, **kwargs) + + +@overload +def teleop(cls: _OpModeType, /) -> _OpModeType: ... + + +@overload +def teleop( + cls: None = None, + /, + *, + name: str | None = None, + group: str | None = None, + description: str | None = None, + text_color: Color | None = None, + background_color: Color | None = None, +) -> Callable[[_OpModeType], _OpModeType]: ... + + +def teleop(_cls=None, **kwargs): + """ + Decorator for automatic registration of teleoperated opmode classes. + + May be used with or without arguments:: + + @teleop + class DefaultTeleMode(PeriodicOpMode): + ... + + @teleop(name="Drive", group="Main") + class DriveMode(PeriodicOpMode): + ... + + ``name`` is shown as the selection name in the Driver Station and must be + unique across teleoperated opmodes in the project. If omitted, the class + name is used. ``group`` controls Driver Station grouping and defaults to + ungrouped. ``description`` is optional. ``text_color`` and + ``background_color`` are optional display colors; both must be provided to + have an effect. + """ + return _make_opmode_decorator(RobotMode.TELEOPERATED, _cls, **kwargs) + + +@overload +def utility(cls: _OpModeType, /) -> _OpModeType: ... + + +@overload +def utility( + cls: None = None, + /, + *, + name: str | None = None, + group: str | None = None, + description: str | None = None, + text_color: Color | None = None, + background_color: Color | None = None, +) -> Callable[[_OpModeType], _OpModeType]: ... + + +def utility(_cls=None, **kwargs): + """ + Decorator for automatic registration of utility opmode classes. + + May be used with or without arguments:: + + @utility + class ServoTest(PeriodicOpMode): + ... + + @utility(name="Servo Test", group="Mechanisms") + class ServoTest(PeriodicOpMode): + ... + + ``name`` is shown as the selection name in the Driver Station and must be + unique across utility opmodes in the project. If omitted, the class name is + used. ``group`` controls Driver Station grouping and defaults to ungrouped. + ``description`` is optional. ``text_color`` and ``background_color`` are + optional display colors; both must be provided to have an effect. + """ + return _make_opmode_decorator(RobotMode.UTILITY, _cls, **kwargs) + + +def _decorator_name(node: ast.expr) -> str | None: + if isinstance(node, ast.Name): + return node.id + if isinstance(node, ast.Call): + return _decorator_name(node.func) + if isinstance(node, ast.Attribute): + return node.attr + return None + + +def _contains_opmode_decorator(path: Path) -> bool: + try: + source = path.read_text() + tree = ast.parse(source, filename=str(path)) + except Exception as exc: + raise RuntimeError( + f"Failed to parse opmode scan file {path.resolve()}: {exc}" + ) from exc + + for stmt in tree.body: + if not isinstance(stmt, ast.ClassDef): + continue + for decorator in stmt.decorator_list: + if _decorator_name(decorator) in _DECORATOR_NAMES: + return True + return False + + +def _iter_scan_files(root: Path): + for path in root.iterdir(): + if path.is_file() and path.suffix == ".py": + yield path + + opmodes_dir = root / "opmodes" + if not opmodes_dir.is_dir(): + return + + for path in opmodes_dir.rglob("*.py"): + yield path + + +def _module_name_from_path(robot_module, scan_root: Path, path: Path) -> str: + relative_path = path.relative_to(scan_root) + parts = list(relative_path.with_suffix("").parts) + if parts and parts[-1] == "__init__": + parts.pop() + + package = robot_module.__spec__.parent if robot_module.__spec__ is not None else "" + if package: + return ".".join([package, *parts]) if parts else package + return ".".join(parts) + + +def _discover_decorated_opmodes(robot: "OpModeRobot") -> None: + robot_module = inspect.getmodule(type(robot)) + if robot_module is None or getattr(robot_module, "__file__", None) is None: + raise RuntimeError( + f"Unable to resolve robot module source file for {type(robot).__module__}.{type(robot).__qualname__}" + ) + + scan_root = Path(robot_module.__file__).resolve().parent + discovered: set[type] = set() + registered_names: set[tuple[RobotMode, str]] = set() + + for path in _iter_scan_files(scan_root): + if not _contains_opmode_decorator(path): + continue + + module_name = _module_name_from_path(robot_module, scan_root, path) + try: + module = importlib.import_module(module_name) + except Exception as exc: + raise RuntimeError( + f"Failed to import opmode discovery candidate {path.resolve()}: {exc}" + ) from exc + for value in vars(module).values(): + if not inspect.isclass(value): + continue + metadata = value.__dict__.get("_wpilib_opmode_metadata") + if metadata is None: + continue + if value in discovered: + continue + if not issubclass(value, OpMode): + raise TypeError( + f"Decorated class {value.__module__}.{value.__qualname__} must inherit from OpMode" + ) + + name = metadata.name or value.__name__ + group = metadata.group or "" + description = metadata.description or "" + registration_key = (metadata.mode, name) + if registration_key in registered_names: + raise ValueError( + f"duplicate opmode registration for mode {metadata.mode} and name {name!r}" + ) + + registered_names.add(registration_key) + discovered.add(value) + robot.add_opmode( + value, + metadata.mode, + name, + group, + description, + metadata.text_color, + metadata.background_color, + ) + + RobotState.publish_opmodes() class OpModeRobot(OpModeRobotBase): @@ -19,10 +310,18 @@ class OpModeRobot(OpModeRobotBase): selected. When no opmode is selected, none_periodic() is called. The driver_station_connected() function is called the first time the driver station connects to the robot. + + Decorated opmodes are auto-discovered from a limited set of Python modules + near the robot class: ``*.py`` files directly beside ``robot.py`` and + ``opmodes/**/*.py`` recursively under an ``opmodes`` subpackage. Any class + decorated with ``@autonomous``, ``@teleop``, or ``@utility`` in those files + is imported, validated as an ``OpMode`` subclass, registered automatically, + and then published to the Driver Station during robot initialization. """ def __init__(self): super().__init__() + _discover_decorated_opmodes(self) def add_opmode( self, From 991fe2f7a064df708c86e93f923a8a3385f557fa Mon Sep 17 00:00:00 2001 From: Dustin Spicuzza Date: Thu, 16 Jul 2026 02:59:07 -0400 Subject: [PATCH 2/2] Add ExpansionHubSample --- .../ExpansionHubSample/defaultautomode.py | 31 +++++++++++++++++++ .../ExpansionHubSample/defaulttelemode.py | 24 ++++++++++++++ examples/robot/ExpansionHubSample/robot.py | 20 ++++++++++++ examples/robot/examples.toml | 1 + 4 files changed, 76 insertions(+) create mode 100644 examples/robot/ExpansionHubSample/defaultautomode.py create mode 100644 examples/robot/ExpansionHubSample/defaulttelemode.py create mode 100644 examples/robot/ExpansionHubSample/robot.py diff --git a/examples/robot/ExpansionHubSample/defaultautomode.py b/examples/robot/ExpansionHubSample/defaultautomode.py new file mode 100644 index 000000000..37e415472 --- /dev/null +++ b/examples/robot/ExpansionHubSample/defaultautomode.py @@ -0,0 +1,31 @@ +# +# 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 wpilib import PeriodicOpMode, Timer +from wpilib.opmoderobot import autonomous + + +@autonomous +class DefaultAutoMode(PeriodicOpMode): + def __init__(self, robot): + super().__init__() + self.robot = robot + self.timer = Timer() + + def start(self): + self.timer.reset() + self.timer.start() + + def periodic(self): + if self.timer.get() < 2.0: + self.robot.motor0.set_throttle(0.5) + self.robot.motor1.set_throttle(0.5) + elif self.timer.get() < 4.0: + self.robot.motor0.set_throttle(0.9) + self.robot.motor1.set_throttle(0.9) + else: + self.robot.motor0.set_throttle(0.0) + self.robot.motor1.set_throttle(0.0) diff --git a/examples/robot/ExpansionHubSample/defaulttelemode.py b/examples/robot/ExpansionHubSample/defaulttelemode.py new file mode 100644 index 000000000..417317c1b --- /dev/null +++ b/examples/robot/ExpansionHubSample/defaulttelemode.py @@ -0,0 +1,24 @@ +# +# 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 wpilib import Gamepad, PeriodicOpMode +from wpilib.opmoderobot import teleop + + +@teleop +class DefaultTeleMode(PeriodicOpMode): + def __init__(self, robot): + super().__init__() + self.robot = robot + self.gamepad = Gamepad(0) + + def periodic(self): + self.robot.motor0.set_throttle(-self.gamepad.get_left_y()) + self.robot.motor1.set_throttle(-self.gamepad.get_right_y()) + self.robot.motor2.set_throttle(-self.gamepad.get_left_x()) + self.robot.motor3.set_throttle(-self.gamepad.get_right_x()) + self.robot.servo0.set_position(self.gamepad.get_left_trigger_axis()) + self.robot.servo1.set_position(self.gamepad.get_right_trigger_axis()) diff --git a/examples/robot/ExpansionHubSample/robot.py b/examples/robot/ExpansionHubSample/robot.py new file mode 100644 index 000000000..34ecaa29d --- /dev/null +++ b/examples/robot/ExpansionHubSample/robot.py @@ -0,0 +1,20 @@ +#!/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 wpilib import ExpansionHubMotor, ExpansionHubServo +from wpilib.opmoderobot import OpModeRobot + + +class Robot(OpModeRobot): + def __init__(self): + super().__init__() + self.motor0 = ExpansionHubMotor(0, 0) + self.motor1 = ExpansionHubMotor(0, 1) + self.motor2 = ExpansionHubMotor(0, 2) + self.motor3 = ExpansionHubMotor(0, 3) + self.servo0 = ExpansionHubServo(0, 0) + self.servo1 = ExpansionHubServo(0, 1) diff --git a/examples/robot/examples.toml b/examples/robot/examples.toml index 35494cfd9..01b0f54e7 100644 --- a/examples/robot/examples.toml +++ b/examples/robot/examples.toml @@ -13,6 +13,7 @@ base = [ "ElevatorSimulation", "ElevatorTrapezoidProfile", "Encoder", + "ExpansionHubSample", "GettingStarted", "Gyro", "DSGamepadChooser",