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.

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 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:

  1. 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.
  2. 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.
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 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.

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);
		}
	}
}