Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Mechanism initial and idle requests #140

Open
wants to merge 1 commit into
base: mf3-dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 65 additions & 0 deletions src/main/java/com/team766/framework3/Mechanism.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import com.team766.logging.Logger;
import com.team766.logging.LoggerExceptionUtils;
import com.team766.logging.Severity;
import edu.wpi.first.wpilibj2.command.Command;
import edu.wpi.first.wpilibj2.command.CommandScheduler;
import edu.wpi.first.wpilibj2.command.SubsystemBase;

Expand All @@ -13,6 +14,41 @@ public abstract class Mechanism<R extends Request<?>> extends SubsystemBase impl
private R request = null;
private boolean isRequestNew = false;

/**
* This Command runs when no other Command (i.e. Procedure) is reserving this Mechanism.
* If this Mechanism defines getIdleRequest, this Command serves to apply that request.
*/
private final class IdleCommand extends Command {
public IdleCommand() {
addRequirements(Mechanism.this);
}

@Override
public void initialize() {
try {
final var r = getIdleRequest();
if (r != null) {
SchedulerMonitor.currentCommand = this;
setRequest(r);
}
} catch (Exception ex) {
ex.printStackTrace();
LoggerExceptionUtils.logException(ex);
} finally {
SchedulerMonitor.currentCommand = null;
}
}

@Override
public boolean isFinished() {
return false;
}
}

public Mechanism() {
setDefaultCommand(new IdleCommand());
}

@Override
public Category getLoggerCategory() {
return Category.MECHANISMS;
Expand All @@ -25,6 +61,32 @@ public final void setRequest(R request) {
log(this.getClass().getName() + " processing request: " + request);
}

/**
* The request returned by this method will be set as the request for this Mechanism when the
* Mechanism is first created.
*
* If this Mechanism defines getIdleRequest(), then this Initial request will only be passed to
* the first call to run() (after that, the Idle request may take over). Otherwise, this Initial
* request will be passed to run() until something calls setRequest() on this Mechanism.
*
* This method will only be called once, immediately before the first call to run(). Because it
* is called before run(), it cannot call getMechanismStatus() or otherwise depend on the Status
* published by this Mechanism.
*/
protected abstract R getInitialRequest();

/**
* The request returned by this method will be set as the request for this Mechanism when no
* Procedures are reserving this Mechanism. This happens when a Procedure which reserved this
* Mechanism completes. It can also happen when a Procedure that reserves this Mechanism is
* preempted by another Procedure, but the new Procedure does not reserve this Mechanism.
* getIdleRequest is especially in the latter case, because it can help to "clean up" after the
* cancelled Procedure, returning this Mechanism back to some safe state.
*/
protected R getIdleRequest() {
return null;
}

protected void checkContextReservation() {
var owningCommand = CommandScheduler.getInstance().requiring(this);
if ((owningCommand == null || SchedulerMonitor.currentCommand != owningCommand)
Expand All @@ -50,6 +112,9 @@ public final void periodic() {

try {
m_runningPeriodic = Thread.currentThread();
if (request == null) {
setRequest(getInitialRequest());
}
boolean wasRequestNew = isRequestNew;
isRequestNew = false;
run(request, wasRequestNew);
Expand Down
2 changes: 1 addition & 1 deletion src/main/java/com/team766/hal/GenericRobotMain3.java
Original file line number Diff line number Diff line change
Expand Up @@ -162,11 +162,11 @@ public void teleopInit() {
public void teleopPeriodic() {
if (faultInRobotInit || faultInTeleopInit) return;

CommandScheduler.getInstance().run();
if (m_oi != null && RobotProvider.instance.hasNewDriverStationData()) {
RobotProvider.instance.refreshDriverStationData();
m_oi.run();
}
CommandScheduler.getInstance().run();
if (m_lights != null && m_lightUpdateLimiter.next()) {
m_lights.run();
}
Expand Down
29 changes: 29 additions & 0 deletions src/test/java/com/team766/TestCase3.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,15 @@
import com.team766.hal.RobotProvider;
import com.team766.hal.mock.TestRobotProvider;
import edu.wpi.first.hal.HAL;
import edu.wpi.first.wpilibj.simulation.DriverStationSim;
import edu.wpi.first.wpilibj2.command.CommandScheduler;
import java.io.IOException;
import java.nio.file.Files;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.extension.ExtendWith;
import org.littletonrobotics.conduit.ConduitApi;
import org.littletonrobotics.junction.inputs.LoggedDriverStation;
import org.littletonrobotics.junction.inputs.LoggedSystemStats;

@ExtendWith(TestLoggerExtension.class)
public abstract class TestCase3 {
Expand All @@ -20,19 +24,44 @@ public abstract class TestCase3 {
public void setUp() {
assert HAL.initialize(500, 0);

setRobotEnabled(true);

ConfigFileTestUtils.resetStatics();
SchedulerUtils.reset();

RobotProvider.instance = new TestRobotProvider();
}

protected void setRobotEnabled(boolean enabled) {
DriverStationSim.setDsAttached(true);
DriverStationSim.setEnabled(enabled);
updateDriverStationData();
}

protected void setRobotAutonomous(boolean autonomous) {
DriverStationSim.setAutonomous(autonomous);
updateDriverStationData();
}

protected void updateDriverStationData() {
// Flush data that was set using DriverStationSim
DriverStationSim.notifyNewData();
// AdvantageKit inserts itself into DriverStation, so we also need to flush AdvantageKit
// data in order for DriverStation to get the updated data.
// https://github.com/Mechanical-Advantage/AdvantageKit/blob/e236e4bf57188addc3befd2d827660b470d9af89/docs/COMMON-ISSUES.md#unit-testing
ConduitApi.getInstance().captureData();
LoggedDriverStation.periodic();
LoggedSystemStats.periodic();
}

protected void loadConfig(String configJson) throws IOException {
var configFilePath = Files.createTempFile("testConfig", ".txt");
Files.writeString(configFilePath, configJson);
ConfigFileReader.instance = new ConfigFileReader(configFilePath.toString());
}

protected void step() {
updateDriverStationData();
CommandScheduler.getInstance().run();
}
}
11 changes: 7 additions & 4 deletions src/test/java/com/team766/framework3/FakeMechanism.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,18 @@ public boolean isDone(FakeStatus status) {
}
}

private FakeRequest currentRequest;
FakeRequest currentRequest;
Boolean wasRequestNew = null;

public FakeRequest currentRequest() {
return currentRequest;
@Override
protected FakeRequest getInitialRequest() {
return new FakeRequest(-1);
}

@Override
protected void run(FakeRequest request, boolean isRequestNew) {
this.currentRequest = request;
currentRequest = request;
wasRequestNew = isRequestNew;
}
}

Expand Down
101 changes: 81 additions & 20 deletions src/test/java/com/team766/framework3/MechanismTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
import com.team766.TestCase3;
import com.team766.framework3.FakeMechanism.FakeRequest;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import org.junit.jupiter.api.Test;

Expand All @@ -15,16 +14,7 @@ public class MechanismTest extends TestCase3 {
/// called from a Procedure which reserves the Mechanism.
@Test
public void testRequests() {
var currentRequest = new AtomicReference<FakeRequest>();
var wasRequestNew = new AtomicBoolean();
var mech =
new Mechanism<FakeRequest>() {
@Override
protected void run(FakeRequest request, boolean isRequestNew) {
currentRequest.set(request);
wasRequestNew.set(isRequestNew);
}
};
var mech = new FakeMechanism();

var cmd =
new ContextImpl(
Expand Down Expand Up @@ -54,23 +44,23 @@ protected void run(FakeRequest request, boolean isRequestNew) {

// Step 1. Test running the Mechanism in its uninitialized state.
step();
assertEquals(null, currentRequest.get());
assertFalse(wasRequestNew.get());
assertEquals(mech.getInitialRequest(), mech.currentRequest);
assertFalse(mech.wasRequestNew);

// Step 2. The Mechanism receives the first request.
step();
assertEquals(new FakeRequest(0), currentRequest.get());
assertTrue(wasRequestNew.get());
assertEquals(new FakeRequest(0), mech.currentRequest);
assertTrue(mech.wasRequestNew);

// Step 3. The Mechanism continues with its first request.
step();
assertEquals(new FakeRequest(0), currentRequest.get());
assertFalse(wasRequestNew.get());
assertEquals(new FakeRequest(0), mech.currentRequest);
assertFalse(mech.wasRequestNew);

// Step 4. The Mechanism receives the second request.
step();
assertEquals(new FakeRequest(1), currentRequest.get());
assertTrue(wasRequestNew.get());
assertEquals(new FakeRequest(1), mech.currentRequest);
assertTrue(mech.wasRequestNew);

// Poke the Procedure to ensure it has finished.
step();
Expand All @@ -82,6 +72,12 @@ protected void run(FakeRequest request, boolean isRequestNew) {
@Test
public void testFailedCheckContextReservationInProcedure() {
class DummyMechanism extends Mechanism<FakeRequest> {
@Override
protected FakeRequest getInitialRequest() {
return new FakeRequest(-1);
}

@Override
protected void run(FakeRequest request, boolean isRequestNew) {}
}
var mech = new DummyMechanism();
Expand Down Expand Up @@ -120,7 +116,7 @@ public void testCheckContextReservationInRun() {
var thrownException = new AtomicReference<Throwable>();
@SuppressWarnings("unused")
var mech =
new Mechanism<FakeRequest>() {
new FakeMechanism() {
@Override
protected void run(FakeRequest request, boolean isRequestNew) {
try {
Expand All @@ -133,4 +129,69 @@ protected void run(FakeRequest request, boolean isRequestNew) {
step();
assertNull(thrownException.get());
}

/// Test that the Initial request runs after Mechanism creation.
@Test
public void testInitialRequest() {
var mech = new FakeMechanism();
step();
assertEquals(new FakeRequest(-1), mech.currentRequest);
assertTrue(mech.wasRequestNew);
// Subsequent steps should continue to pass the Initial request to run(),
// but it should not be indicated as a new request.
mech.currentRequest = null;
step();
assertEquals(new FakeRequest(-1), mech.currentRequest);
assertFalse(mech.wasRequestNew);
}

/// Test that the Idle request runs if no other Command reserves this Mechanism.
@Test
public void testIdleRequest() {
var mech =
new FakeMechanism() {
@Override
protected FakeRequest getIdleRequest() {
return new FakeRequest(0);
}
};

// The first step should run the Initial request.
step();
assertEquals(new FakeRequest(-1), mech.currentRequest);
assertTrue(mech.wasRequestNew);
// On subsequent steps, the Idle request should take over (as long as a Command hasn't
// reserved this Mechanism).
step();
assertEquals(new FakeRequest(0), mech.currentRequest);
assertTrue(mech.wasRequestNew);
// Subsequent steps should continue to pass the Idle request to run(),
// but it should not be indicated as a new request.
mech.currentRequest = null;
step();
assertEquals(new FakeRequest(0), mech.currentRequest);
assertFalse(mech.wasRequestNew);

// When a Command is scheduled which reserves this Procedure, it should preempt
// the Idle request.
new FunctionalProcedure(
Set.of(mech),
context -> {
mech.setRequest(new FakeRequest(1));
context.waitFor(() -> false);
})
.createCommandToRunProcedure()
.schedule();
step();
step(); // NOTE: Second step() is needed because Scheduler runs Procedures after Subsystems
assertEquals(new FakeRequest(1), mech.currentRequest);
assertTrue(mech.wasRequestNew);
// Subsequent steps should allow the scheduled Command to continue. It should not be
// interrupted by the Idle request, even if the scheduled Command does not set a
// new request.
mech.currentRequest = null;
step();
assertEquals(new FakeRequest(1), mech.currentRequest);
assertFalse(mech.wasRequestNew);
}
}
Loading