Skip to content

Commit 8765dcb

Browse files
committed
Further clean up, expose feedback decorator
This moves the feedback decorator to the magicbot.magic_tunable module, where it makes more sense to live. This also exposes the feedback decorator, as it was never exported. Add a hint in the docs to also see LiveWindow, as that may fit teams' needs without the need for this. Also delete the dead autosend code, as this is what it ended up being. Finally, this cleans up and fixes a few gripes I had: * The NetworkTables key used didn't match those of tunables. * Decorating methods in your robot class with feedback didn't work. * The decorator required the use of an extra set of parentheses.
1 parent 6a34403 commit 8765dcb

File tree

3 files changed

+100
-109
lines changed

3 files changed

+100
-109
lines changed

magicbot/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11

22
from .magicrobot import MagicRobot
3-
from .magic_tunable import tunable
3+
from .magic_tunable import feedback, tunable
44
from .magic_reset import will_reset_to
55

66
from .state_machine import AutonomousStateMachine, StateMachine, default_state, state, timed_state

magicbot/magic_tunable.py

Lines changed: 92 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import functools
2+
import inspect
13

24
from networktables import NetworkTables
35
from ntcore.value import Value
@@ -123,34 +125,93 @@ def setup_tunables(component, cname, prefix='components'):
123125
prop._ntattr = ntattr
124126

125127

126-
# TODO
127-
#def autosend(f=None):
128-
# '''
129-
# Decorator used to send variables to DS::
130-
#
131-
#
132-
#
133-
# class MyComponent:
134-
#
135-
# @autosend
136-
# def my_sensor(self):
137-
# return self.limit_switch.get()
138-
#
139-
# ...
140-
#
141-
# class MyRobot:
142-
#
143-
# mine = MyComponent
144-
#
145-
# This will cause the output of this function to be sent
146-
# to ``/components/mine/my_sensor``
147-
# '''
148-
#
149-
#
150-
# def _get(self):
151-
# return self._Magicbot__autosend.get(f)
152-
#
153-
# prop = _AutosendProperty(fget=_get)
154-
# prop.f = f
155-
#
156-
# return prop
128+
def feedback(f=None, *, key: str = None):
129+
"""
130+
This decorator allows you to create NetworkTables values that are
131+
automatically updated with the return value of a method.
132+
133+
``key`` is an optional parameter, and if it is not supplied,
134+
the key will default to the method name with a leading ``get_`` removed.
135+
If the method does not start with ``get_``, the key will be the full
136+
name of the method.
137+
138+
The key of the NetworkTables value will vary based on what kind of
139+
object the decorated method belongs to:
140+
141+
* A component: ``/components/COMPONENTNAME/VARNAME``
142+
* Your main robot class: ``/robot/VARNAME``
143+
144+
The NetworkTables value will be auto-updated in all modes (except test mode).
145+
146+
.. warning:: The function should only act as a getter, and must not
147+
take any arguments (other than self).
148+
149+
Example::
150+
151+
from magicbot import feedback
152+
153+
class MyComponent:
154+
navx: ...
155+
156+
@feedback
157+
def get_angle(self):
158+
return self.navx.getYaw()
159+
160+
class MyRobot(magicbot.MagicRobot):
161+
my_component: MyComponent
162+
163+
...
164+
165+
In this example, the NetworkTable key is stored at
166+
``/components/my_component/angle``.
167+
168+
.. seealso:: :class:`~wpilib.livewindow.LiveWindow` may suit your needs,
169+
especially if you wish to monitor WPILib objects.
170+
171+
.. versionadded:: 2018.1.0
172+
"""
173+
if f is None:
174+
return functools.partial(feedback, key=key)
175+
176+
if not callable(f):
177+
raise TypeError('Illegal use of feedback decorator on non-callable {!r}'.format(f))
178+
sig = inspect.signature(f)
179+
name = f.__name__
180+
181+
if len(sig.parameters) != 1:
182+
raise ValueError("{} may not take arguments other than 'self' (must be a simple getter method)".format(name))
183+
184+
# Set attributes to be checked during injection
185+
f.__feedback__ = True
186+
f.__key__ = key
187+
188+
return f
189+
190+
191+
def collect_feedbacks(component, cname: str, prefix='components'):
192+
"""
193+
Finds all methods decorated with :func:`feedback` on an object
194+
and returns a list of 2-tuples (method, NetworkTables entry).
195+
196+
.. note:: This isn't useful for normal use.
197+
"""
198+
if prefix is None:
199+
prefix = '/%s' % cname
200+
else:
201+
prefix = '/%s/%s' % (prefix, cname)
202+
203+
feedbacks = []
204+
205+
for name, method in inspect.getmembers(component, inspect.ismethod):
206+
if getattr(method, '__feedback__', False):
207+
key = method.__key__
208+
if key is None:
209+
if name.startswith('get_'):
210+
key = name[4:]
211+
else:
212+
key = name
213+
214+
entry = NetworkTables.getEntry('%s/%s' % (prefix, key))
215+
feedbacks.append((method, entry))
216+
217+
return feedbacks

magicbot/magicrobot.py

Lines changed: 7 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -13,64 +13,11 @@
1313

1414
from networktables import NetworkTables
1515

16-
from .magic_tunable import setup_tunables, _TunableProperty
16+
from .magic_tunable import setup_tunables, _TunableProperty, collect_feedbacks
1717
from .magic_reset import will_reset_to
1818

1919
__all__ = ['MagicRobot']
2020

21-
def feedback(key=None):
22-
"""
23-
If this decorator is applied to a function,
24-
its return value will automatically be sent
25-
to NetworkTables at key ``/robot/components/component/key``.
26-
27-
``key`` is an optional parameter, and if it is not supplied,
28-
the key will default to the method name with 'get_' removed.
29-
If the method does not start with 'get_', the key will be the full
30-
name of the method.
31-
32-
.. note:: The function will automatically be called in disabled,
33-
autonomous, and teleop.
34-
35-
.. warning:: The function should only act as a getter, and accept
36-
no arguments.
37-
38-
Example::
39-
40-
class Component1:
41-
42-
@feedback()
43-
def get_angle(self):
44-
return self.navx.getYaw()
45-
46-
In this example, the NetworkTable key is stored at
47-
``/robot/components/component1/angle``.
48-
"""
49-
def decorator(func):
50-
if not callable(func):
51-
raise ValueError('Illegal use of feedback decorator on non-callable {!r}'.format(func))
52-
sig = inspect.signature(func)
53-
name = func.__name__
54-
55-
if len(sig.parameters) != 1:
56-
raise ValueError("{} may not take arguments other than 'self' (must be a simple getter method)".format(name))
57-
58-
nt_key = key
59-
if nt_key is None:
60-
# If no key is passed, get key from method name.
61-
# -1 instead of 1 in case 'get_' is not present,
62-
# in which case the key will be the method name
63-
64-
if name.startswith('get_'):
65-
nt_key = name[4:]
66-
else:
67-
nt_key = name
68-
# Set '__feedback__ attribute to be checked during injection
69-
func.__feedback__ = True
70-
# Store key within the function to avoid using class dictionary
71-
func.__key__ = nt_key
72-
return func
73-
return decorator
7421

7522
class MagicRobot(wpilib.SampleRobot,
7623
metaclass=OrderedClass):
@@ -380,8 +327,6 @@ def operatorControl(self):
380327

381328
while self.isOperatorControl() and self.isEnabled():
382329

383-
#self._update_autosend()
384-
385330
try:
386331
self.teleopPeriodic()
387332
except:
@@ -504,6 +449,7 @@ def _create_components(self):
504449
for cname, component in components:
505450
self._components.append(component)
506451
setup_tunables(component, cname, 'components')
452+
self._feedbacks += collect_feedbacks(component, cname, 'components')
507453
self._setup_vars(cname, component)
508454
self._setup_reset_vars(component)
509455

@@ -515,6 +461,7 @@ def _create_components(self):
515461

516462
# And for self too
517463
setup_tunables(self, 'robot', None)
464+
self._feedbacks += collect_feedbacks(self, 'robot', None)
518465

519466
# Call setup functions for components
520467
for cname, component in components:
@@ -532,7 +479,6 @@ def _create_component(self, name, ctyp):
532479

533480
# Automatically inject a logger object
534481
component.logger = logging.getLogger(name)
535-
component._Magicbot__autosend = {}
536482

537483
self.logger.info("-> %s (class: %s)", name, ctyp.__name__)
538484

@@ -585,10 +531,6 @@ def _setup_vars(self, cname, component):
585531

586532
self._inject(n, inject_type, cname, component)
587533

588-
for (name, method) in inspect.getmembers(component, predicate=inspect.ismethod):
589-
if getattr(method, '__feedback__', False):
590-
self._feedbacks.append((component, cname, name))
591-
592534
def _inject(self, n, inject_type, cname, component):
593535
# Retrieve injectable object
594536
injectable = self._injectables.get(n)
@@ -610,11 +552,6 @@ def _inject(self, n, inject_type, cname, component):
610552
setattr(component, n, injectable)
611553
self.logger.debug("-> %s as %s.%s", injectable, cname, n)
612554

613-
# XXX
614-
#if is_autosend:
615-
# where to store the nt key?
616-
# component._Magicbot__autosend[prop.f] = None
617-
618555
def _setup_reset_vars(self, component):
619556
reset_dict = {}
620557

@@ -629,22 +566,15 @@ def _setup_reset_vars(self, component):
629566
if reset_dict:
630567
component.__dict__.update(reset_dict)
631568
self._reset_components.append((reset_dict, component))
632-
633-
#def _update_autosend(self):
634-
# # seems like this should just be a giant list instead
635-
# for component in self._components:
636-
# d = component._Magicbot__autosend
637-
# for f in d.keys():
638-
# d[f] = f(component)
639569

640570
def _update_feedback(self):
641-
for (component, cname, name) in self._feedbacks:
571+
for method, entry in self._feedbacks:
642572
try:
643-
func = getattr(component, name)
573+
value = method()
644574
except:
575+
self.onException()
645576
continue
646-
# Put ntvalue at /robot/components/component/key
647-
self.__nt.putValue('/components/{0}/{1}'.format(cname, func.__key__), func())
577+
entry.setValue(value)
648578

649579
def _execute_components(self):
650580
for component in self._components:

0 commit comments

Comments
 (0)