Features are the main draw of Dairy, and they power a lot of the additional
utilities provided by Core and other libraries.
Features 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.
publicclassFeatureOverviewimplementsFeature{{// returns true if this is currently active// true means it will receive updates for the current OpModeboolean isActive =isActive();}// we won't look at the dependency system closely hereprivateDependency<?> dependency =(VoidDependency)(Wrapper opMode,List<?extendsFeature> resolvedFeatures,boolean yielding)->{};@NonNull@OverridepublicDependency<?>getDependency(){return dependency;}@OverridepublicvoidsetDependency(@NonNullDependency<?> dependency){this.dependency = dependency;}//// Hooks//// By default, all the hooks are empty, so you only need to override the ones you want to use@OverridepublicvoidpreUserInitHook(Wrapper opMode){}@OverridepublicvoidpostUserInitHook(Wrapper opMode){}@OverridepublicvoidpreUserInitLoopHook(Wrapper opMode){}@OverridepublicvoidpostUserInitLoopHook(Wrapper opMode){}@OverridepublicvoidpreUserStartHook(Wrapper opMode){}@OverridepublicvoidpostUserStartHook(Wrapper opMode){}@OverridepublicvoidpreUserLoopHook(Wrapper opMode){}@OverridepublicvoidpostUserLoopHook(Wrapper opMode){}@OverridepublicvoidpreUserStopHook(Wrapper opMode){}@OverridepublicvoidpostUserStopHook(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.@Overridepublicvoidcleanup(Wrapper opMode){}{// finally, lets look at some Feature related FeatureRegistrar methodsFeatureRegistrar.getActiveFeatures();// list of currently active featuresFeatureRegistrar.getRegisteredFeatures();// list of registered featuresFeatureRegistrar.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 isFeatureRegistrar.registerFeature(this);// same as Feature.register()FeatureRegistrar.deregisterFeature(this);// same as Feature.deregister()}}
The ‘pre’ hooks run in order of Features being activated, while ‘post’ hooks
run in the reverse order. This ensures that less important Features are always
enclosed by the code of the more important ones.
This is important to keep in mind when writing a Feature.
Features generally fall into two categories:
Dynamic: Features that are instantiated during an OpMode, generally during
init. These Features 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: Features 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.
publicfinalclassBulkReadsimplementsFeature{// 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"privateDependency<?> dependency =newSingleAnnotation<>(Attach.class);// getters and setters for dependency@NonNull@OverridepublicDependency<?>getDependency(){return dependency;}@OverridepublicvoidsetDependency(@NonNullDependency<?> dependency){this.dependency = dependency;}// we'll make the constructor privateprivateJavaBulkReads(){}// our singleton instancepublicstaticfinalJavaBulkReadsINSTANCE=newJavaBulkReads();privateList<LynxModule> modules;@OverridepublicvoidpreUserInitHook(@NonNullWrapper 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,@OverridepublicvoidpreUserInitLoopHook(@NonNullWrapper opMode){ modules.forEach(LynxModule::clearBulkCache);}@OverridepublicvoidpreUserStartHook(@NonNullWrapper opMode){ modules.forEach(LynxModule::clearBulkCache);}@OverridepublicvoidpreUserLoopHook(@NonNullWrapper opMode){ modules.forEach(LynxModule::clearBulkCache);}// cleanup is a guaranteed run post stop// here, we'll drop our references to the modules@Overridepublicvoidcleanup(@NonNullWrapper opMode){ modules =null;}// the @BulkReads.Attach annotation@Target(ElementType.TYPE)@Retention(RetentionPolicy.RUNTIME)public@interfaceAttach{}}
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 Controllers, 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.
publicclassPIDimplementsFeature{// 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!privateDependency<?> dependency =Yielding.INSTANCE;@NonNull@OverridepublicDependency<?>getDependency(){return dependency;}@OverridepublicvoidsetDependency(@NonNullDependency<?> dependency){this.dependency = dependency;}publicPID(/* encoder, motor, coefficients... */){// store them...}{// regardless of constructor used, call register when the class is instantiatedregister();}privatevoidupdate(){// calculate next output using encoder, target and coefficients// don't update motor power if the controller isn't enabledif(!enabled)return;// set motor power to calculated output}// users should be able to change the targetprivateint target =0;publicintgetTarget(){return target;}publicvoidsetTarget(int target){this.target = target;}// users should be able to enable / disable the controllerprivateboolean enabled =true;publicbooleanisEnabled(){return enabled;}publicvoidsetEnabled(boolean enabled){this.enabled = enabled;}// after init loop and loop we will update the controller@OverridepublicvoidpostUserInitLoopHook(@NonNullWrapper opMode){update();}@OverridepublicvoidpostUserLoopHook(@NonNullWrapper opMode){update();}// in cleanup we deregister, which prevents this from sticking around for another OpMode,// unless the user calls register again@Overridepublicvoidcleanup(@NonNullWrapper 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.LogDependencyResolutionExceptionspublicclassUsageextendsOpMode{publicUsage(){// 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 initprivatefinalOpModeLazyCell<PID> pidCell =newOpModeLazyCell<>(PID::new);privatePIDgetPID(){return pidCell.get();}@Overridepublicvoidinit(){}// we can set the target in loop if we want// and we don't need to worry about anything else!@Overridepublicvoidloop(){if(gamepad1.a){getPID().setTarget(100);}elseif(gamepad1.b){getPID().setTarget(0);}}}