Skip to content

Commit 1544c7b

Browse files
author
Krypton Cougars
committed
Adds flowcontrol metaprogramming module
1 parent ef0da50 commit 1544c7b

File tree

3 files changed

+723
-0
lines changed

3 files changed

+723
-0
lines changed

commandbased/conditionalcommand.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
from wpilib.command.commandgroup import CommandGroup
2+
3+
class ConditionalCommand(CommandGroup):
4+
5+
def __init__(self, name, onTrue=None, onFalse=None):
6+
super().__init__(name)
7+
8+
self.onTrue = self._processCommand(onTrue)
9+
self.onFalse = self._processCommand(onFalse)
10+
11+
12+
def _processCommand(self, cmd):
13+
if cmd is None:
14+
return []
15+
16+
with self.mutex:
17+
if self.locked:
18+
raise ValueError('Cannot add conditions to ConditionalCommand')
19+
20+
for reqt in cmd.getRequirements():
21+
self.requires(reqt)
22+
23+
cmd.setParent(self)
24+
cmd = CommandGroup.Entry(
25+
cmd,
26+
CommandGroup.Entry.IN_SEQUENCE,
27+
None
28+
)
29+
return [cmd]
30+
31+
32+
def _initialize(self):
33+
super()._initialize()
34+
35+
if self.condition():
36+
self.commands = self.onTrue
37+
else:
38+
self.commands = self.onFalse

commandbased/flowcontrol.py

Lines changed: 303 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,303 @@
1+
"""
2+
These functions can be used to make programming CommandGroups much more
3+
intuitive. For more information, check each method's docstring.
4+
"""
5+
6+
from wpilib.command import Command, CommandGroup
7+
from commandbased.conditionalcommand import ConditionalCommand
8+
from commandbased.cancelcommand import CancelCommand
9+
10+
import inspect
11+
12+
__all__ = ['IF', 'ELIF', 'ELSE', 'WHILE', 'RETURN', 'BREAK']
13+
14+
15+
def _getCommandGroup():
16+
"""
17+
Does some rather ugly stack inspection to find out which CommandGroup the
18+
calling function was triggered from.
19+
"""
20+
# https://stackoverflow.com/a/14694234
21+
stack = inspect.stack()
22+
frame = stack[2].frame
23+
while 'self' not in frame.f_locals:
24+
frame = frame.f_back
25+
if frame is None:
26+
raise ValueError(
27+
'Could not find calling class for %s' %
28+
stack[1].function
29+
)
30+
31+
cg = frame.f_locals['self']
32+
if not isinstance(cg, CommandGroup):
33+
raise ValueError(
34+
'%s may not be used outside of a CommandGroup' %
35+
stack[1].function
36+
)
37+
38+
return cg
39+
40+
41+
def _getSource(parent):
42+
"""Finds the highest level CommandGroup of parent"""
43+
44+
try:
45+
return parent._source
46+
except AttributeError:
47+
return parent
48+
49+
50+
def _buildCommandGroup(func, parent):
51+
"""Turns the given function into a full CommandGroup."""
52+
53+
source = _getSource(parent)
54+
55+
cg = CommandGroup(func.__name__)
56+
cg._source = source
57+
func(cg)
58+
59+
return cg
60+
61+
62+
def _restartWhile(self):
63+
"""
64+
Replaces isFinished for a ConditionalCommand in a WHILE loop, to keep it
65+
running.
66+
"""
67+
if self.forceCancel:
68+
self.forceCancel = False
69+
return True
70+
71+
finished = super(ConditionalCommand, self).isFinished()
72+
73+
if finished:
74+
if self.condition():
75+
self.currentCommandIndex = None
76+
return False
77+
78+
return finished
79+
80+
81+
def _popIfStack(cg):
82+
"""
83+
We buffer conditionals until the last moment so we don't have trouble with
84+
Commands being locked when they're added to a CommandGroup.
85+
"""
86+
if cg._ifStack:
87+
top = cg._ifStack.pop(0)
88+
cmd = None
89+
for x in reversed(cg._ifStack):
90+
if x[0]:
91+
cmd = ConditionalCommand('flowcontrolELIF', x[1], cmd)
92+
cmd.condition = x[0]
93+
94+
else:
95+
cmd = x[1]
96+
97+
cmd = ConditionalCommand('flowcontrolIF', top[1], cmd)
98+
cmd.condition = top[0]
99+
100+
cg._addSequential(cmd)
101+
cg._ifStack = None
102+
103+
104+
# These _hook methods ensure we always add our buffered conditions
105+
def _hookSequential(self, cmd, timeout=None):
106+
_popIfStack(self)
107+
self._addSequential(cmd, timeout)
108+
109+
110+
def _hookParallel(self, cmd, timeout=None):
111+
_popIfStack(self)
112+
self._addParallel(cmd, timeout)
113+
114+
115+
def _hookStart(self):
116+
_popIfStack(self)
117+
self._start()
118+
119+
120+
def _hookParent(self, parent):
121+
_popIfStack(self)
122+
self._setParent(parent)
123+
124+
125+
def _hookCommandGroup(cg):
126+
"""Override some methods of the CommandGroup to add buffered commands."""
127+
128+
try:
129+
cg._ifStack
130+
return
131+
except AttributeError:
132+
pass
133+
134+
cg._addSequential = cg.addSequential
135+
cg._addParallel = cg.addParallel
136+
cg._start = cg.start
137+
cg._setParent = cg.setParent
138+
139+
cg.addSequential = _hookSequential.__get__(cg)
140+
cg.addParallel = _hookParallel.__get__(cg)
141+
cg.start = _hookStart.__get__(cg)
142+
cg.setParent = _hookParent.__get__(cg)
143+
144+
145+
def IF(condition):
146+
"""
147+
Use as a decorator for a function. That function will be placed into a
148+
CommandGroup and run inside a ConditionalCommand with the given condition.
149+
The decorated function must accept one positional argument that will be used
150+
as its 'self'.
151+
"""
152+
153+
def flowcontrolIF(func):
154+
parent = _getCommandGroup()
155+
_hookCommandGroup(parent)
156+
157+
cg = _buildCommandGroup(func, parent)
158+
159+
try:
160+
_popIfStack(parent)
161+
except AttributeError:
162+
pass
163+
164+
parent._ifStack = [(condition, cg)]
165+
166+
return flowcontrolIF
167+
168+
169+
def ELIF(condition):
170+
"""
171+
Use as a decorator for a function. That function will be placed into a
172+
CommandGroup which will be triggered by a ConditionalCommand that uses the
173+
passed condition. That ConditionalCommand will then be added as the onFalse
174+
for the ConditionalCommand created by a previous IF or ELIF.
175+
"""
176+
177+
def flowcontrolELIF(func):
178+
parent = _getCommandGroup()
179+
180+
cg = _buildCommandGroup(func, parent)
181+
182+
try:
183+
parent._ifStack.append((condition, cg))
184+
except AttributeError:
185+
raise ValueError('Cannot use ELIF without IF')
186+
187+
return flowcontrolELIF
188+
189+
190+
def ELSE(func):
191+
"""
192+
Use as a decorator for a function. That function will be placed into a
193+
CommandGroup which will be added as the onFalse for the ConditionalCommand
194+
created by a previous IF or ELIF.
195+
"""
196+
197+
parent = _getCommandGroup()
198+
cg = _buildCommandGroup(func, parent)
199+
200+
try:
201+
parent._ifStack.append((None, cg))
202+
except AttributeError:
203+
raise ValueError('Cannot use ELSE without IF')
204+
205+
_popIfStack(parent)
206+
207+
208+
def WHILE(condition):
209+
"""
210+
Use as a decorator for a function. That function will be placed into a
211+
CommandGroup, which will be added to a ConditionalCommand. It will be
212+
modified to restart itself automatically.
213+
"""
214+
215+
def flowcontrolWHILE(func):
216+
parent = _getCommandGroup()
217+
source = _getSource(parent)
218+
219+
try:
220+
parentLoop = source._currentLoop
221+
except AttributeError:
222+
parentLoop = None
223+
224+
cg = CommandGroup(func.__name__)
225+
cg._source = source
226+
227+
# Set the current loop for any BREAK statements
228+
source._currentLoop = cg
229+
func(cg)
230+
source._currentLoop = parentLoop
231+
232+
def cancelLoop(self):
233+
self.getGroup().forceCancel = True
234+
235+
end = Command('END WHILE')
236+
end.initialize = cancelLoop.__get__(end)
237+
238+
cond = ConditionalCommand('flowcontrolWHILE', cg, end)
239+
cond.condition = condition
240+
cond.forceCancel = False
241+
cond.isFinished = _restartWhile.__get__(cond)
242+
cond._parentLoop = parentLoop
243+
244+
parent.addSequential(cond)
245+
246+
cg.conditionalCommand = cond
247+
248+
return flowcontrolWHILE
249+
250+
251+
def RETURN():
252+
"""
253+
Calling this function will end the source CommandGroup immediately.
254+
"""
255+
256+
parent = _getCommandGroup()
257+
source = _getSource(parent)
258+
259+
parent.addSequential(CancelCommand(source))
260+
261+
262+
def BREAK(steps=1):
263+
"""
264+
Calling this function will end the loop that contains it. Pass an integer to
265+
break out of that number of nested loops.
266+
"""
267+
268+
if steps < 1:
269+
raise ValueError('Steps to BREAK cannot be < 1')
270+
271+
parent = _getCommandGroup()
272+
source = _getSource(parent)
273+
274+
try:
275+
loop = source._currentLoop
276+
except AttributeError:
277+
raise ValueError('Cannot BREAK outside of a loop')
278+
279+
if loop is None:
280+
raise ValueError('Cannot BREAK outside of a loop')
281+
282+
# We can't use CancelCommand here, because the conditionalCommand attribute
283+
# isn't yet bound to the CommandGroup, so we find it at initialization time
284+
# instead
285+
def cancelLoop():
286+
nonlocal loop, steps
287+
step = 1
288+
while steps > step:
289+
loop = loop.conditionalCommand._parentLoop
290+
291+
if loop is None:
292+
raise ValueError(
293+
'BREAK %i not possible with loop depth %i' %
294+
(steps, step)
295+
)
296+
297+
step += 1
298+
299+
loop.conditionalCommand.forceCancel = True
300+
301+
breakLoop = Command('flowcontrolBREAK')
302+
breakLoop.initialize = cancelLoop
303+
parent.addSequential(breakLoop)

0 commit comments

Comments
 (0)