Feature
s are the main draw of Dairy, and they power a lot of the additional
utilities provided by Core and other libraries.
Feature
s add a lot of additional hooks to the OpMode lifecycle, and are
capable of doing a lot of powerful work and tasks in the background of user
code.
A hook is an update callback. Basically, when an event occurs in code, if that
code is written to expose hooks, it will all all the hooks which are attached to
that event.
Dairy adds hooks to the OpMode life cycle, which means that when your OpMode
code runs (init
, init_loop
, start
, loop
, stop
), Dairy makes it so that
other Objects in code receive updates about these events occurring, without any
added code for you.
If you are using OpMode, you don’t need to add any code, if you are using
LinearOpMode, then additional lines of code to enable the hooks MUST be added.
public class FeatureOverview implements Feature {
{
// returns true if this is currently active
// true means it will receive updates for the current OpMode
boolean isActive = isActive();
}
// we won't look at the dependency system closely here
private Dependency<?> dependency = (VoidDependency) (Wrapper opMode, List<? extends Feature> resolvedFeatures, boolean yielding) -> {};
@NonNull
@Override
public Dependency<?> getDependency() {
return dependency;
}
@Override
public void setDependency(@NonNull Dependency<?> dependency) {
this.dependency = dependency;
}
//
// Hooks
//
// By default, all the hooks are empty, so you only need to override the ones you want to use
@Override
public void preUserInitHook(Wrapper opMode) {}
@Override
public void postUserInitHook(Wrapper opMode) {}
@Override
public void preUserInitLoopHook(Wrapper opMode) {}
@Override
public void postUserInitLoopHook(Wrapper opMode) {}
@Override
public void preUserStartHook(Wrapper opMode) {}
@Override
public void postUserStartHook(Wrapper opMode) {}
@Override
public void preUserLoopHook(Wrapper opMode) {}
@Override
public void postUserLoopHook(Wrapper opMode) {}
@Override
public void preUserStopHook(Wrapper opMode) {}
@Override
public void postUserStopHook(Wrapper opMode) {}
// cleanup differs from postUserStopHook, it runs after the OpMode has completely stopped,
// and is guaranteed to run, even if the OpMode stopped from a crash.
@Override
public void cleanup(Wrapper opMode) {}
{
// finally, lets look at some Feature related FeatureRegistrar methods
FeatureRegistrar.getActiveFeatures(); // list of currently active features
FeatureRegistrar.getRegisteredFeatures(); // list of registered features
FeatureRegistrar.isFeatureActive(this); // boolean, same as Feature.isActive()
// don't register and deregister Features a lot, its expensive
// try to keep this to only during construction / init, or only one or two at runtime
// the more you do, the more expensive it is
FeatureRegistrar.registerFeature(this); // same as Feature.register()
FeatureRegistrar.deregisterFeature(this); // same as Feature.deregister()
}
}
The ‘pre’ hooks run in order of Feature
s being activated, while ‘post’ hooks
run in the reverse order. This ensures that less important Feature
s are always
enclosed by the code of the more important ones.
This is important to keep in mind when writing a Feature
.
Feature
s generally fall into two categories:
- Dynamic:
Feature
s that are instantiated during an OpMode, generally during
init
. These Feature
s generally register
themselves with the
FeatureRegistrar
when they are instantiated, or it must be done manually.
Oftentimes they are meant to only exist for one OpMode, and so deregister
themselves when it ends.
- Static:
Feature
s that are global singletons and are registered via
Sinister
, rather than by themselves. They exist all the time, and use the
Dependency
system, usually with annotations to attach to OpModes on demand.
Dairy offers utilities that come in both forms.
In your own code, you might use the static singleton approach for subsystems, if
you’re doing them yourself, rather than with Mercurial
, or a big global
utility system.
In comparison, the dynamic approach is good for when the utility is needed on
demand, and just needs to receive updates about the OpMode.
We’ll take a look at both the static and the dynamic approach.
Static:
- Utility to automatically manage manual BulkReads.
- Enabled for an OpMode by adding
@BulkReads.Attach
.
This utility is available via the Curdled library.
These classes are from the featuredev.[jdoc|kdoc]
package of the Dairy
examples.
public final class BulkReads implements Feature {
// first, we need to set up the dependency
// this makes a rule that says:
// "for this feature to receive updates about an OpMode, it must have @BulkReads.Attach"
private Dependency<?> dependency = new SingleAnnotation<>(Attach.class);
// getters and setters for dependency
@NonNull
@Override
public Dependency<?> getDependency() {
return dependency;
}
@Override
public void setDependency(@NonNull Dependency<?> dependency) {
this.dependency = dependency;
}
// we'll make the constructor private
private JavaBulkReads() {}
// our singleton instance
public static final JavaBulkReads INSTANCE = new JavaBulkReads();
private List<LynxModule> modules;
@Override
public void preUserInitHook(@NonNull Wrapper opMode) {
// collect and store the modules
modules = opMode.getOpMode().hardwareMap.getAll(LynxModule.class);
// set them to manual
modules.forEach(lynxModule -> lynxModule.setBulkCachingMode(LynxModule.BulkCachingMode.MANUAL));
}
// now, in each pre phase, we'll clear the bulk cache
// we do this in pre, as most calculations and updates happen during
// post,
@Override
public void preUserInitLoopHook(@NonNull Wrapper opMode) {
modules.forEach(LynxModule::clearBulkCache);
}
@Override
public void preUserStartHook(@NonNull Wrapper opMode) {
modules.forEach(LynxModule::clearBulkCache);
}
@Override
public void preUserLoopHook(@NonNull Wrapper opMode) {
modules.forEach(LynxModule::clearBulkCache);
}
// cleanup is a guaranteed run post stop
// here, we'll drop our references to the modules
@Override
public void cleanup(@NonNull Wrapper opMode) {
modules = null;
}
// the @BulkReads.Attach annotation
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Attach {}
}
Dynamic:
- Utility to automatically update a PID in the background.
- Enabled for an OpMode by instantiating it.
Core has far more advanced support for PID controllers, go take a look.
These classes are from the featuredev.[jdoc|kdoc]
package of the Dairy
examples.
I’m not going to actually write the proper code this time, as would add a decent
number of lines of code to the examples, and it would just add noise to what
this is being demonstrated. If you don’t like Dairy Controller
s, then you
could finish off this class and use it. This could easily be done with a
pre-done PID class from another library, or you could fill in the blanks here by
writing it yourself, it isn’t hard.
public class PID implements Feature {
// first, we need to set up the dependency
// Yielding just says "this isn't too important, always attach me, but run me after more important things"
// Yielding is reusable!
private Dependency<?> dependency = Yielding.INSTANCE;
@NonNull
@Override
public Dependency<?> getDependency() {
return dependency;
}
@Override
public void setDependency(@NonNull Dependency<?> dependency) {
this.dependency = dependency;
}
public PID(/* encoder, motor, coefficients... */) {
// store them...
}
{
// regardless of constructor used, call register when the class is instantiated
register();
}
private void update() {
// calculate next output using encoder, target and coefficients
// don't update motor power if the controller isn't enabled
if (!enabled) return;
// set motor power to calculated output
}
// users should be able to change the target
private int target = 0;
public int getTarget() {
return target;
}
public void setTarget(int target) {
this.target = target;
}
// users should be able to enable / disable the controller
private boolean enabled = true;
public boolean isEnabled() {
return enabled;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
// after init loop and loop we will update the controller
@Override
public void postUserInitLoopHook(@NonNull Wrapper opMode) {
update();
}
@Override
public void postUserLoopHook(@NonNull Wrapper opMode) {
update();
}
// in cleanup we deregister, which prevents this from sticking around for another OpMode,
// unless the user calls register again
@Override
public void cleanup(@NonNull Wrapper opMode) {
deregister();
}
}
Now, lets look at how to use these in an OpMode:
These classes are from the featuredev.[jdoc|kdoc]
package of the Dairy
examples.
// we add this, and BulkReads will receive updates for this OpMode
// which means it will handle bulk reads for us!
@BulkReads.Attach
// this annotation makes it so that the FeatureRegistrar will log all the reasons
// for any registered Features that weren't activated
@FeatureRegistrar.LogDependencyResolutionExceptions
public class Usage extends OpMode {
public Usage() {
// instead of `@FeatureRegistrar.LogDependencyResolutionExceptions`
// checkFeatures can be used to ensure that all features
// passed to the function will be activated,
// or will throw an error for them specifically
// both are good debugging tools!
FeatureRegistrar.checkFeatures(BulkReads.INSTANCE/*, varargs Features*/);
}
// we'll look at OpModeLazyCell later, but this means that this PID will be instantiated in init for us
// for this example it doesn't really matter, but if we actually implemented it, then we would need to use this
// to ensure that we don't access the hardware map until init
private final OpModeLazyCell<PID> pidCell = new OpModeLazyCell<>(PID::new);
private PID getPID() { return pidCell.get(); }
@Override
public void init() {
}
// we can set the target in loop if we want
// and we don't need to worry about anything else!
@Override
public void loop() {
if (gamepad1.a) {
getPID().setTarget(100);
}
else if (gamepad1.b) {
getPID().setTarget(0);
}
}
}