Skip to content

Commit

Permalink
Add builder pattern and fix tests for Monitor
Browse files Browse the repository at this point in the history
  • Loading branch information
aidnem committed Nov 6, 2024
1 parent c0c0471 commit c8b81bb
Show file tree
Hide file tree
Showing 2 changed files with 221 additions and 71 deletions.
144 changes: 138 additions & 6 deletions monitors/src/main/java/coppercore/monitors/Monitor.java
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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,
Expand All @@ -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
Expand All @@ -40,7 +63,7 @@ public void periodic(double currentTimeSeconds) {
triggeredTime = currentTimeSeconds;
}

if (currentTimeSeconds - triggeredTime > timeToFault) {
if (currentTimeSeconds - triggeredTime >= timeToFault) {
faulted = true;
}
} else {
Expand All @@ -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);
}
}
}
148 changes: 83 additions & 65 deletions monitors/src/test/java/MonitorTests.java
Original file line number Diff line number Diff line change
@@ -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());
}
}

0 comments on commit c8b81bb

Please sign in to comment.