diff --git a/monitors/src/main/java/coppercore/monitors/Monitor.java b/monitors/src/main/java/coppercore/monitors/Monitor.java index 0744573..23ba7c7 100644 --- a/monitors/src/main/java/coppercore/monitors/Monitor.java +++ b/monitors/src/main/java/coppercore/monitors/Monitor.java @@ -3,7 +3,7 @@ import java.util.function.BooleanSupplier; public class Monitor { - String name; // Name to log the status of the monitor under + String name; // Name to log the status of the monitor under, used by MonitoredSubsystem boolean sticky; // Should the monitor still report a fault after conditions return to normal? double timeToFault; // How long the value can be unacceptable before a fault occurs BooleanSupplier isStateValid; // Supplier with which to check whether the value is acceptable @@ -15,6 +15,22 @@ public class Monitor { boolean triggered = false; // Is the value currently unnacceptable? boolean faulted = false; // Has the monitor detected a fault? + /** + * Creates a fault Monitor. This constructor takes all parameters at once. There is also a + * builder pattern supplied under MonitorBuilder. Using the builder is recommended because it + * makes code much more readable, but is not required. + * + * @param name the name of the monitor, which will be used by MonitoredSubsystem for logging. + * @param sticky whether the fault should remain faulted after conditions return to an + * acceptable state. + * @param isStateValid supplier for whether the state is CURRENTLY valid. This doesn't need to + * handle persistence, the monitor class will handle this automatically. + * @param timeToFault the time, in seconds, that isStateValid must return false before the a + * fault is triggered. + * @param faultCallback a function called on every periodic loop while the monitor is in a + * faulted state. + * @see MonitorBuilder + */ public Monitor( String name, boolean sticky, @@ -29,6 +45,13 @@ public Monitor( this.faultCallback = faultCallback; } + /** + * This is the "main loop" of the Monitor. This should be called each in each periodic loop. + * + * @param currentTimeSeconds the current timestamp in seconds. This doesn't need to have a + * specific frame of reference, it is used to detect when conditions have been unnacceptable + * for enough time to fault. + */ public void periodic(double currentTimeSeconds) { // currentTimeSeconds doesn't need to be from a specific point in history // As long as the reference point is always the same, it could be from @@ -40,7 +63,7 @@ public void periodic(double currentTimeSeconds) { triggeredTime = currentTimeSeconds; } - if (currentTimeSeconds - triggeredTime > timeToFault) { + if (currentTimeSeconds - triggeredTime >= timeToFault) { faulted = true; } } else { @@ -53,21 +76,130 @@ public void periodic(double currentTimeSeconds) { if (faulted) { faultCallback.run(); } - - // TODO: Move logging to MonitoredSubsystem under wpi interface - // Logger.recordOutput("monitors/" + name + "/triggered", triggered); - // Logger.recordOutput("monitors/" + name + "/faulted", faulted); } + /** + * Returns a boolean describing whether or not the monitor is faulted. A monitor is considered + * faulted when it has been triggered for a time greater than or equal to its time to fault. A + * monitor is also considered faulted if it is sticky and has ever been faulted. + * + * @return whether or not the monitor is currently faulted. + */ public boolean isFaulted() { return faulted; } + /** + * Returns a boolean describing whether or not the monitor is currently triggered. A monitor is + * triggered when isStateValid returns false. + * + * @return whether or not the monitor is currently triggered + */ public boolean isTriggered() { return triggered; } + /** + * Reset a sticky fault. This means that, if a Monitor is sticky, and is currently faulted, + * calling this function will return it to a non-faulted state. + */ public void resetStickyFault() { faulted = false; } + + /** + * Get the name of the Monitor. + * + * @return a string, the name of the monitor that it uses for logging. + */ + public String getName() { + return name; + } + + /** + * This class is meant to build a fault monitor. Create a builder, then call withName, + * withStickyness, withTimeToFault, and withIsStateValid, and withFaultCallback to configure its + * fields. Once every field is configured, call build() to return a shiny new fault monitor. + */ + public static class MonitorBuilder { + String name; // Name to log the status of the monitor under + boolean sticky; // Should the monitor still report a fault after conditions return to + // normal? + double timeToFault; // How long the value can be unacceptable before a fault occurs + BooleanSupplier + isStateValid; // Supplier with which to check whether the value is acceptable + Runnable faultCallback; // Function to call when the fault happens + + /** + * Sets the name of the monitor. This name will be used when the monitor is logged by + * MonitoredSubsystem. + * + * @param name the name of the monitor, which is used for logging by MonitoredSubsystem or + * can be used for manual logging outside of a MonitoredSubsystem. + * @return the monitor builder, so that successive builder calls can be chained + */ + public MonitorBuilder withName(String name) { + this.name = name; + return this; + } + + /** + * Sets whether or not the monitor is sticky or not, and returns itself. + * + * @param sticky a boolean, whether or not the monitor should remain faulted after + * conditions return to normal. + * @return the monitor builder, so that successive builder calls can be chained. + */ + public MonitorBuilder withStickyness(boolean sticky) { + this.sticky = sticky; + return this; + } + + /** + * Sets how long the monitor may be triggered before it faults, and returns itself. + * + * @param timeToFault a double, how long the monitor can be triggered (in an unnacceptable + * state) before it becomes faulted in seconds. + * @return the monitor builder, so that successive builder calls can be chained. + */ + public MonitorBuilder withTimeToFault(double timeToFault) { + this.timeToFault = timeToFault; + return this; + } + + /** + * Sets the supplier for whether or not the state is currently valid. + * + * @param isStateValid a boolean supplier, which should return true when the state is valid + * and false when the state is invalid. This supplier doesn't need to account for + * timeToFault, this is automatically handled by the monitor. + * @return the monitor, so that successive builder calls can be chained. + */ + public MonitorBuilder withIsStateValidSupplier(BooleanSupplier isStateValid) { + this.isStateValid = isStateValid; + return this; + } + + /** + * Sets how long the monitor may be triggered before it faults, and returns itself. + * + * @param faultCallback a runnable, which will be called periodic unnacceptable state) + * before it becomes faulted in seconds. + * @return the monitor builder, so that successive builder calls can be chained. + */ + public MonitorBuilder withFaultCallback(Runnable faultCallback) { + this.faultCallback = faultCallback; + return this; + } + + /** + * Instantiates a monitor and returns it. This method should be called after all of the + * fields of the monitor are configured using with[Field] methods. + * + * @return a monitor with the fields set by the builder. + */ + public Monitor build() { + return new Monitor(name, sticky, isStateValid, timeToFault, faultCallback); + } + } } diff --git a/monitors/src/test/java/MonitorTests.java b/monitors/src/test/java/MonitorTests.java index 4543f17..42dd1a3 100644 --- a/monitors/src/test/java/MonitorTests.java +++ b/monitors/src/test/java/MonitorTests.java @@ -1,73 +1,91 @@ package coppercore.monitors.test; +import coppercore.monitors.Monitor; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + public class MonitorTests { // TODO: FIGURE OUT HOW TO TEST THIS // Callbacks are hard :( // Mimics a changing robot state - // boolean isStateValid = true; - - // @Test - // public void nonStickyTest() { - // Monitor exampleMonitor = - // new Monitor("exampleMonitor", false, () -> isStateValid, 1.0, () -> {}); - - // exampleMonitor.periodic(0.0); - // Assertions.assertFalse(exampleMonitor.isFaulted()); - // Assertions.assertFalse(exampleMonitor.isTriggered()); - - // isStateValid = false; - // exampleMonitor.periodic(1.0); - // Assertions.assertFalse(exampleMonitor.isFaulted()); - // Assertions.assertFalse(exampleMonitor.isTriggered()); - - // exampleMonitor.periodic(1.25); - // Assertions.assertFalse(exampleMonitor.isFaulted()); - // Assertions.assertFalse(exampleMonitor.isTriggered()); - - // exampleMonitor.periodic(2.0); - // Assertions.assertTrue(exampleMonitor.isFaulted()); - // Assertions.assertTrue(exampleMonitor.isTriggered()); - - // isStateValid = true; - // exampleMonitor.periodic(3.0); - // Assertions.assertFalse(exampleMonitor.isFaulted()); - // Assertions.assertFalse(exampleMonitor.isTriggered()); - // } - - // @Test - // public void stickyTest() { - // // Mimics a changing robot state - // boolean isStateValid = true; - - // Monitor exampleMonitor = - // new Monitor( - // "exampleMonitor", - // true, - // () -> isStateValid, - // 1.0, - // () -> {}); // TODO: Figure out how to test callback - - // exampleMonitor.periodic(0.0); - // Assertions.assertFalse(exampleMonitor.isFaulted()); - // Assertions.assertFalse(exampleMonitor.isTriggered()); - - // isStateValid = false; - // exampleMonitor.periodic(1.0); - // Assertions.assertFalse(exampleMonitor.isFaulted()); - // Assertions.assertFalse(exampleMonitor.isTriggered()); - - // exampleMonitor.periodic(1.25); - // Assertions.assertFalse(exampleMonitor.isFaulted()); - // Assertions.assertFalse(exampleMonitor.isTriggered()); - - // exampleMonitor.periodic(2.0); - // Assertions.assertTrue(exampleMonitor.isFaulted()); - // Assertions.assertTrue(exampleMonitor.isTriggered()); - - // isStateValid = true; - // exampleMonitor.periodic(3.0); - // Assertions.assertTrue(exampleMonitor.isFaulted()); - // Assertions.assertFalse(exampleMonitor.isTriggered()); - // } + private boolean isStateValid = true; + + private boolean getIsStateValid() { + return isStateValid; + } + + @Test + public void nonStickyTest() { + isStateValid = true; + + Monitor exampleMonitor = + new Monitor.MonitorBuilder() + .withName("exampleMonitor") + .withStickyness(false) + .withIsStateValidSupplier(() -> getIsStateValid()) + .withTimeToFault(1.0) + .withFaultCallback(() -> {}) + .build(); + + exampleMonitor.periodic(0.0); + Assertions.assertFalse(exampleMonitor.isFaulted()); + Assertions.assertFalse(exampleMonitor.isTriggered()); + + isStateValid = false; + exampleMonitor.periodic(1.0); + Assertions.assertFalse(exampleMonitor.isFaulted()); + Assertions.assertTrue(exampleMonitor.isTriggered()); + + exampleMonitor.periodic(1.25); + Assertions.assertFalse(exampleMonitor.isFaulted()); + Assertions.assertTrue(exampleMonitor.isTriggered()); + + exampleMonitor.periodic(2.0); + Assertions.assertTrue(exampleMonitor.isFaulted()); + Assertions.assertTrue(exampleMonitor.isTriggered()); + + isStateValid = true; + exampleMonitor.periodic(3.0); + Assertions.assertFalse(exampleMonitor.isFaulted()); + Assertions.assertFalse(exampleMonitor.isTriggered()); + } + + @Test + public void stickyTest() { + // Mimics a changing robot state + isStateValid = true; + + Monitor exampleMonitor = + new Monitor.MonitorBuilder() + .withName("exampleMonitor") + .withStickyness(true) + .withIsStateValidSupplier(() -> getIsStateValid()) + .withTimeToFault(1.0) + .withFaultCallback(() -> {}) + .build(); + + exampleMonitor.periodic(0.0); + Assertions.assertFalse(exampleMonitor.isFaulted()); + Assertions.assertFalse(exampleMonitor.isTriggered()); + + isStateValid = false; + exampleMonitor.periodic(1.0); + Assertions.assertFalse(exampleMonitor.isFaulted()); + Assertions.assertFalse(getIsStateValid()); + Assertions.assertTrue(exampleMonitor.isTriggered()); + + exampleMonitor.periodic(1.25); + Assertions.assertFalse(exampleMonitor.isFaulted()); + Assertions.assertTrue(exampleMonitor.isTriggered()); + + exampleMonitor.periodic(2.0); + Assertions.assertTrue(exampleMonitor.isFaulted()); + Assertions.assertTrue(exampleMonitor.isTriggered()); + + isStateValid = true; + exampleMonitor.periodic(3.0); + Assertions.assertTrue(exampleMonitor.isFaulted()); + Assertions.assertFalse(exampleMonitor.isTriggered()); + } }