Skip to content

First version of iterativerobotpy.py #161

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 31 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
c454e6d
First version of iterativerobotpy.py
MikeStitt Apr 5, 2025
7042b96
Simulates, but does not update robot position in simulation.
MikeStitt Apr 8, 2025
3f1e12c
Simulates, but does not update robot position in simulation.
MikeStitt Apr 8, 2025
5f793b7
Only call the inits on a transition.
MikeStitt Apr 8, 2025
21bab63
This version does simulate in autonomous and teleop okay. Need to tes…
MikeStitt Apr 9, 2025
ff1713e
Cleaning Up
MikeStitt Apr 25, 2025
0c86080
Cleaning up.
MikeStitt Apr 26, 2025
2a17935
Fix typos
MikeStitt Apr 26, 2025
9429b8c
Correct periodS to be period, use from . to import
MikeStitt Apr 27, 2025
2f0c2e1
Fix up watchdog interface.
MikeStitt Apr 27, 2025
958c909
Cleaning up.
MikeStitt Apr 27, 2025
7522ec2
Cleaning up code.
MikeStitt Apr 28, 2025
3b36199
Running through black
MikeStitt Apr 28, 2025
c5ae00a
Added docstrings, new todo's based upon docstring comparison.
MikeStitt Apr 29, 2025
6553746
Added missing wpimath.units.second
MikeStitt Apr 29, 2025
0adee5a
Fix typo, second to seconds
MikeStitt Apr 29, 2025
55c5e5d
use if rather than match for python 3.9
MikeStitt Apr 30, 2025
0eb421f
Merge branch 'upstream/main' into pyIterativeAndTimedRobot
MikeStitt Apr 30, 2025
6cf2977
simulationPeriodic becomes _simulationPeriodic
MikeStitt Apr 30, 2025
9e27f0a
Get the physics engine to work.
MikeStitt Apr 30, 2025
0cf4834
Fix units bug for getLoopStartTime. now returns microseconds.
MikeStitt Apr 30, 2025
1b009f5
Make work with python 3.9
MikeStitt Apr 30, 2025
d06fe46
Add test of period math, and improve comments.
MikeStitt Apr 30, 2025
4c0baa4
fix black formatting
MikeStitt May 1, 2025
12c90b6
Proof of Concept for TimedRobot Functional Tests
MikeStitt May 3, 2025
4c2379c
WIP still sims.
MikeStitt May 9, 2025
ca1e7e1
Improving simulation.
MikeStitt May 10, 2025
df22b32
Merge branch 'main' into pocTimedRobotFunctionalTests
MikeStitt May 10, 2025
5dd838c
Getting new tests to not break existing tests.
MikeStitt May 11, 2025
acc7385
Limit setuptools versions
MikeStitt May 12, 2025
4cce809
Progress on tests.
MikeStitt May 12, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion rdev_requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ packaging
pydantic<2,!=1.10.20
pytest
requests
setuptools
setuptools < 80.0
setuptools_scm >= 6.2, < 8
tomlkit
tomli
Expand Down
149 changes: 149 additions & 0 deletions subprojects/robotpy-wpilib/tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,25 @@
import wpilib
from wpilib.simulation._simulation import _resetWpilibSimulationData

from pathlib import Path

import gc

import weakref

import hal
import hal.simulation
import wpilib.shuffleboard
from wpilib.simulation import DriverStationSim, pauseTiming, restartTiming
import wpilib.simulation
from pyfrc.test_support.controller import TestController
from pyfrc.physics.core import PhysicsInterface

try:
import commands2
except ImportError:
commands2 = None


@pytest.fixture
def cfg_logging(caplog):
Expand All @@ -29,3 +48,133 @@ def nt(cfg_logging, wpilib_state):
finally:
instance.stopLocal()
instance._reset()


@pytest.fixture(scope="class", autouse=False)
def physics_and_decorated_robot_class(
myrobot_class, robots_sim_enable_physics
) -> tuple:
# attach physics

robotClass = myrobot_class
physicsInterface = None
if robots_sim_enable_physics:
physicsInterface, robotClass = PhysicsInterface._create_and_attach(
myrobot_class,
Path(__file__).parent,
)

if physicsInterface:
physicsInterface.log_init_errors = False

# Tests need to know when robotInit is called, so override the robot
# to do that
class TestRobot(robotClass):
def robotInit(self):
try:
super().robotInit()
finally:
self.__robotInitialized()

TestRobot.__name__ = robotClass.__name__
TestRobot.__module__ = robotClass.__module__
TestRobot.__qualname__ = robotClass.__qualname__

return (physicsInterface, TestRobot)


@pytest.fixture(scope="function", autouse=False)
def robot_with_sim_setup_teardown(physics_and_decorated_robot_class):
"""
Your robot instance

.. note:: RobotPy/WPILib testing infrastructure is really sensitive
to ensuring that things get cleaned up properly. Make sure
that you don't store references to your robot or other
WPILib objects in a global or static context.
"""

#
# This function needs to do the same things that RobotBase.main does
# plus some extra things needed for testing
#
# Previously this was separate from robot fixture, but we need to
# ensure that the robot cleanup happens deterministically relative to
# when handle cleanup/etc happens, otherwise unnecessary HAL errors will
# bubble up to the user
#

nt_inst = ntcore.NetworkTableInstance.getDefault()
nt_inst.startLocal()

pauseTiming()
restartTiming()

wpilib.DriverStation.silenceJoystickConnectionWarning(True)
DriverStationSim.setAutonomous(False)
DriverStationSim.setEnabled(False)
DriverStationSim.notifyNewData()

robot = physics_and_decorated_robot_class[1]()

# Tests only get a proxy to ensure cleanup is more reliable
yield weakref.proxy(robot)

# If running in separate processes, no need to do cleanup
# if ISOLATED:
# # .. and funny enough, in isolated mode we *don't* want the
# # robot to be cleaned up, as that can deadlock
# self._saved_robot = robot
# return

# reset engine to ensure it gets cleaned up too
# -> might be holding wpilib objects, or the robot
if physics_and_decorated_robot_class[0]:
physics_and_decorated_robot_class[0].engine = None

# HACK: avoid motor safety deadlock
wpilib.simulation._simulation._resetMotorSafety()

del robot

if commands2 is not None:
commands2.CommandScheduler.resetInstance()

# Double-check all objects are destroyed so that HAL handles are released
gc.collect()

# shutdown networktables before other kinds of global cleanup
# -> some reset functions will re-register listeners, so it's important
# to do this before so that the listeners are active on the current
# NetworkTables instance
nt_inst.stopLocal()
nt_inst._reset()

# Cleanup WPILib globals
# -> preferences, SmartDashboard, Shuffleboard, LiveWindow, MotorSafety
wpilib.simulation._simulation._resetWpilibSimulationData()
wpilib._wpilib._clearSmartDashboardData()
wpilib.shuffleboard._shuffleboard._clearShuffleboardData()

# Cancel all periodic callbacks
hal.simulation.cancelAllSimPeriodicCallbacks()

# Reset the HAL handles
hal.simulation.resetGlobalHandles()

# Reset the HAL data
hal.simulation.resetAllSimData()

# Don't call HAL shutdown! This is only used to cleanup HAL extensions,
# and functions will only be called the first time (unless re-registered)
# hal.shutdown()


@pytest.fixture(scope="function")
def getTestController(
reraise, robot_with_sim_setup_teardown: wpilib.RobotBase
) -> TestController:
"""
A pytest fixture that provides control over your robot_with_sim_setup_teardown
"""
return TestController(reraise, robot_with_sim_setup_teardown)
112 changes: 112 additions & 0 deletions subprojects/robotpy-wpilib/tests/test_poc_timedrobot.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
"""
Proof of concept to test TimedRobotPy using PyTest

This POC was made by deconstructing pytest_plugin.py so that it is no longer a plugin but a class that provides
fixtures.

To run / debug this:

pytest subprojects/robotpy-wpilib/tests/test_poc_timedrobot.py --no-header -vvv -s

"""

import pytest

from wpilib.timedrobotpy import TimedRobotPy


def run_practice(control: "TestController"):
"""Runs through the entire span of a practice match"""

with control.run_robot():
# Run disabled for a short period
control.step_timing(seconds=0.5, autonomous=True, enabled=False)

# Run autonomous + enabled for 15 seconds
control.step_timing(seconds=15, autonomous=True, enabled=True)

# Disabled for another short period
control.step_timing(seconds=0.5, autonomous=False, enabled=False)

# Run teleop + enabled for 2 minutes
control.step_timing(seconds=120, autonomous=False, enabled=True)


class TestThings:

class MyRobot(TimedRobotPy):
def __init__(self):
super().__init__(period=0.020)
self.robotInitialized = False
self.robotPeriodicCount = 0

#########################################################
## Common init/update for all modes
def robotInit(self):
self.robotInitialized = True

def robotPeriodic(self):
self.robotPeriodicCount += 1

#########################################################
## Autonomous-Specific init and update
def autonomousInit(self):
pass

def autonomousPeriodic(self):
pass

def autonomousExit(self):
pass

#########################################################
## Teleop-Specific init and update
def teleopInit(self):
pass

def teleopPeriodic(self):
pass

#########################################################
## Disabled-Specific init and update
def disabledPeriodic(self):
pass

def disabledInit(self):
pass

#########################################################
## Test-Specific init and update
def testInit(self):
pass

def testPeriodic(self):
pass

@classmethod
@pytest.fixture(scope="class", autouse=True)
def myrobot_class(cls) -> type[MyRobot]:
return cls.MyRobot

@classmethod
@pytest.fixture(scope="class", autouse=True)
def robots_sim_enable_physics(cls) -> bool:
return False

def test_iterative(self, getTestController, robot_with_sim_setup_teardown):
"""Ensure that all states of the iterative robot run"""
assert robot_with_sim_setup_teardown.robotInitialized == False
assert robot_with_sim_setup_teardown.robotPeriodicCount == 0
run_practice(getTestController)

assert robot_with_sim_setup_teardown.robotInitialized == True
assert robot_with_sim_setup_teardown.robotPeriodicCount > 0

def test_iterative_again(self, getTestController, robot_with_sim_setup_teardown):
"""Ensure that all states of the iterative robot run"""
assert robot_with_sim_setup_teardown.robotInitialized == False
assert robot_with_sim_setup_teardown.robotPeriodicCount == 0
run_practice(getTestController)

assert robot_with_sim_setup_teardown.robotInitialized == True
assert robot_with_sim_setup_teardown.robotPeriodicCount > 0
41 changes: 41 additions & 0 deletions subprojects/robotpy-wpilib/tests/test_timedrobot.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import pytest

from wpilib.timedrobotpy import _Callback


def test_calcFutureExpirationUs() -> None:
cb = _Callback(func=None, periodUs=20_000, expirationUs=100)
assert cb.calcFutureExpirationUs(100) == 20_100
assert cb.calcFutureExpirationUs(101) == 20_100
assert cb.calcFutureExpirationUs(20_099) == 20_100
assert cb.calcFutureExpirationUs(20_100) == 40_100
assert cb.calcFutureExpirationUs(20_101) == 40_100

cb = _Callback(func=None, periodUs=40_000, expirationUs=500)
assert cb.calcFutureExpirationUs(500) == 40_500
assert cb.calcFutureExpirationUs(501) == 40_500
assert cb.calcFutureExpirationUs(40_499) == 40_500
assert cb.calcFutureExpirationUs(40_500) == 80_500
assert cb.calcFutureExpirationUs(40_501) == 80_500

cb = _Callback(func=None, periodUs=1_000, expirationUs=0)
assert (
cb.calcFutureExpirationUs(1_000_000_000_000_000_000)
== 1_000_000_000_000_001_000
)
assert (
cb.calcFutureExpirationUs(1_000_000_000_000_000_001)
== 1_000_000_000_000_001_000
)
assert (
cb.calcFutureExpirationUs(1_000_000_000_000_000_999)
== 1_000_000_000_000_001_000
)
assert (
cb.calcFutureExpirationUs(1_000_000_000_000_001_000)
== 1_000_000_000_000_002_000
)
assert (
cb.calcFutureExpirationUs(1_000_000_000_000_001_001)
== 1_000_000_000_000_002_000
)
4 changes: 3 additions & 1 deletion subprojects/robotpy-wpilib/wpilib/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,8 @@

from .cameraserver import CameraServer
from .deployinfo import getDeployData
from .iterativerobotpy import IterativeRobotPy
from .timedrobotpy import TimedRobotPy

try:
from .version import version as __version__
Expand All @@ -241,4 +243,4 @@

from ._impl.main import run

__all__ += ["CameraServer", "run"]
__all__ += ["CameraServer", "run", "IterativeRobotPy", "TimedRobotPy"]
Loading
Loading