Skip to content

Commit 9ef1bc0

Browse files
authored
Merge pull request #94 from auscompgeek/cleanup-feedback
Further clean up, expose feedback decorator
2 parents 7acbbe1 + 8765dcb commit 9ef1bc0

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)