Skip to content

Commit 12dfc59

Browse files
author
Newton Crosby
committed
Adds PIDCommand to the Commands2 framework. robotpy#28
1 parent f48b3fe commit 12dfc59

File tree

4 files changed

+189
-0
lines changed

4 files changed

+189
-0
lines changed

commands2/__init__.py

+2
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
from .paralleldeadlinegroup import ParallelDeadlineGroup
3131
from .parallelracegroup import ParallelRaceGroup
3232
from .perpetualcommand import PerpetualCommand
33+
from .pidcommand import PIDCommand
3334
from .printcommand import PrintCommand
3435
from .proxycommand import ProxyCommand
3536
from .proxyschedulecommand import ProxyScheduleCommand
@@ -60,6 +61,7 @@
6061
"ParallelDeadlineGroup",
6162
"ParallelRaceGroup",
6263
"PerpetualCommand",
64+
"PIDCommand",
6365
"PrintCommand",
6466
"ProxyCommand",
6567
"ProxyScheduleCommand",

commands2/pidcommand.py

+62
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
# Copyright (c) FIRST and other WPILib contributors.
2+
# Open Source Software; you can modify and/or share it under the terms of
3+
# the WPILib BSD license file in the root directory of this project.
4+
from __future__ import annotations
5+
6+
from wpimath.controller import PIDController
7+
from .subsystem import Subsystem
8+
from .command import Command
9+
from typing import Set, Callable, Union
10+
11+
12+
class PIDCommand(Command):
13+
"""
14+
A command that controls an output with a :class:`.PIDController`. Runs forever by default - to add
15+
exit conditions and/or other behavior, subclass this class. The controller calculation and output
16+
are performed synchronously in the command's execute() method.
17+
18+
This class is provided by the NewCommands VendorDep
19+
"""
20+
21+
def __init__(
22+
self,
23+
controller: PIDController,
24+
measurement_source: Callable[[], float],
25+
setpoint_source: Union[float, Callable[[], float]],
26+
use_output: Callable[[float], None],
27+
*requirements: Subsystem,
28+
):
29+
"""Creates a new PIDCommand, which controls the given output with a PIDController.
30+
31+
:param controller: the controller that controls the output.
32+
:param measurementSource: the measurement of the process variable
33+
:param setpointSource: the controller's setpoint
34+
:param useOutput: the controller's output
35+
:param requirements: the subsystems required by this command
36+
"""
37+
super().__init__()
38+
self.controller = controller
39+
self.use_output = use_output
40+
self.measurement = measurement_source
41+
self.setpoint = setpoint_source
42+
self.requirements: Set[Subsystem] = set(requirements)
43+
44+
def initialize(self):
45+
self.controller.reset()
46+
47+
def execute(self):
48+
set_point = (
49+
self.setpoint() if isinstance(self.setpoint, Callable) else self.setpoint
50+
)
51+
52+
self.use_output(self.controller.calculate(self.measurement(), set_point))
53+
54+
def end(self, interrupted: bool):
55+
self.use_output(0)
56+
57+
def get_controller(self) -> PIDController:
58+
"""Returns the PIDController used by the command.
59+
60+
:returns: The PIDController
61+
"""
62+
return self.controller

tests/test_pidcommand.py

+96
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
from typing import TYPE_CHECKING
2+
3+
from util import * # type: ignore
4+
import wpimath.controller as controller
5+
import commands2
6+
7+
if TYPE_CHECKING:
8+
from .util import *
9+
10+
import pytest
11+
12+
13+
def test_pidCommandSupplier(scheduler: commands2.CommandScheduler):
14+
with ManualSimTime() as sim:
15+
output_float = OOFloat(0.0)
16+
measurement_source = OOFloat(5.0)
17+
setpoint_source = OOFloat(2.0)
18+
pid_controller = controller.PIDController(.1, .01, .001)
19+
system = commands2.Subsystem()
20+
pidCommand = commands2.PIDCommand(pid_controller, measurement_source, setpoint_source, output_float.set, system)
21+
start_spying_on(pidCommand)
22+
scheduler.schedule(pidCommand)
23+
scheduler.run()
24+
sim.step(1)
25+
scheduler.run()
26+
27+
assert scheduler.isScheduled(pidCommand)
28+
29+
assert not pidCommand.controller.atSetpoint()
30+
31+
# Tell the pid command we're at our setpoint through the controller
32+
measurement_source.set(setpoint_source())
33+
34+
sim.step(2)
35+
36+
scheduler.run()
37+
38+
# Should be measuring error of 0 now
39+
assert pidCommand.controller.atSetpoint()
40+
41+
42+
def test_pidCommandScalar(scheduler: commands2.CommandScheduler):
43+
with ManualSimTime() as sim:
44+
output_float = OOFloat(0.0)
45+
measurement_source = OOFloat(5.0)
46+
setpoint_source = 2.0
47+
pid_controller = controller.PIDController(.1, .01, .001)
48+
system = commands2.Subsystem()
49+
pidCommand = commands2.PIDCommand(pid_controller, measurement_source, setpoint_source, output_float.set, system)
50+
start_spying_on(pidCommand)
51+
scheduler.schedule(pidCommand)
52+
scheduler.run()
53+
sim.step(1)
54+
scheduler.run()
55+
56+
assert scheduler.isScheduled(pidCommand)
57+
58+
assert not pidCommand.controller.atSetpoint()
59+
60+
# Tell the pid command we're at our setpoint through the controller
61+
measurement_source.set(setpoint_source)
62+
63+
sim.step(2)
64+
65+
scheduler.run()
66+
67+
# Should be measuring error of 0 now
68+
assert pidCommand.controller.atSetpoint()
69+
70+
71+
def test_withTimeout(scheduler: commands2.CommandScheduler):
72+
with ManualSimTime() as sim:
73+
output_float = OOFloat(0.0)
74+
measurement_source = OOFloat(5.0)
75+
setpoint_source = OOFloat(2.0)
76+
pid_controller = controller.PIDController(.1, .01, .001)
77+
system = commands2.Subsystem()
78+
command1 = commands2.PIDCommand(pid_controller, measurement_source, setpoint_source, output_float.set, system)
79+
start_spying_on(command1)
80+
81+
timeout = command1.withTimeout(2)
82+
83+
scheduler.schedule(timeout)
84+
scheduler.run()
85+
86+
verify(command1).initialize()
87+
verify(command1).execute()
88+
assert not scheduler.isScheduled(command1)
89+
assert scheduler.isScheduled(timeout)
90+
91+
sim.step(3)
92+
scheduler.run()
93+
94+
verify(command1).end(True)
95+
verify(command1, never()).end(False)
96+
assert not scheduler.isScheduled(timeout)

tests/util.py

+29
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,35 @@ def __call__(self) -> bool:
141141
return self.pressed
142142

143143

144+
class OOFloat:
145+
def __init__(self, value: float = 0.0) -> None:
146+
self.value = value
147+
148+
def get(self) -> float:
149+
return self.value
150+
151+
def set(self, value: float):
152+
self.value = value
153+
154+
def incrementAndGet(self) -> float:
155+
self.value += 1
156+
return self.value
157+
158+
def addAndGet(self, value: float) -> float:
159+
self.value += value
160+
return self.value
161+
162+
def __eq__(self, value: float) -> bool:
163+
return self.value == value
164+
165+
def __lt__(self, value: float) -> bool:
166+
return self.value < value
167+
168+
def __call__(self) -> float:
169+
return self.value
170+
171+
def __name__(self) -> str:
172+
return "OOFloat"
144173
##########################################
145174
# Fakito Framework
146175

0 commit comments

Comments
 (0)