Context
This program will control a robot. It is written in Java 7 and compiles into an Android app, but these classes are not Android specific. This program was written for FIRST Tech Challenge and uses some classes from their SDK, available online at https://github.com/ftctechnh/ftc_app.
Purpose
These classes are designed to provide event handling capabilities to the robot so that it can more easily react to user input.
Details
Gamepad.on(ButtonState, Button, EventListener)
is called as the program is initializing and allows the user to set an event listener in the case of an event on a button. There is a loop that runs for as long as the robot is running. Before each iteration of this loop, the member variables of the instances of com.qualcomm.robotcore.hardware.Gamepad
, such as a
and b
(public boolean
), are updated to reflect if some button is currently being pressed. Then, Gamepad.handleEvents()
is called to update the state of the buttons and run user defined event handler code.
Gamepad.java: This is the main class that the user interacts with.
package org.firstinspires.ftc.teamcode.teleop;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
public class Gamepad {
private Map<Button, EventContainer> mButtonEvents = new HashMap<>();
private com.qualcomm.robotcore.hardware.Gamepad mGamepad;
public Gamepad(com.qualcomm.robotcore.hardware.Gamepad gamepad) {
mGamepad = gamepad;
}
public void on(ButtonState event, Button button, EventListener listener) {
if (!mButtonEvents.containsKey(button)) {
mButtonEvents.put(button, new EventContainer());
}
EventContainer eventContainer = mButtonEvents.get(button);
eventContainer.addHandler(event, listener);
}
public void handleEvents() {
Set<Button> buttons = mButtonEvents.keySet();
for (Button button : buttons) {
EventContainer eventContainer = mButtonEvents.get(button);
eventContainer.nextState(button.extract(mGamepad)).handle();
}
}
}
Button.java: This enum contains every button on the physical controller (all are not listed here, but follow the same pattern that those listed do). It also contains the code needed to get if the button is being pressed from the com.qualcomm.robotcore.hardware.Gamepad
instance.
package org.firstinspires.ftc.teamcode.teleop;
import com.qualcomm.robotcore.hardware.Gamepad;
public enum Button {
A(new ExtractButton() {
@Override
public boolean extract(com.qualcomm.robotcore.hardware.Gamepad gamepad) {
return gamepad.a;
}
}),
B(new ExtractButton() {
@Override
public boolean extract(com.qualcomm.robotcore.hardware.Gamepad gamepad) {
return gamepad.b;
}
// More buttons here
});
ExtractButton mExtractor;
Button(ExtractButton extractor) {
mExtractor = extractor;
}
public boolean extract(com.qualcomm.robotcore.hardware.Gamepad gamepad) {
return mExtractor.extract(gamepad);
}
private interface ExtractButton {
boolean extract(com.qualcomm.robotcore.hardware.Gamepad gamepad);
}
}
EventContainer.java: This class implements a state machine and manages adding and calling event handlers.
package org.firstinspires.ftc.teamcode.teleop;
import java.util.HashMap;
import java.util.Map;
public class EventContainer {
private ButtonState mState;
private Map<ButtonState, EventListener> mEventHandlers = new HashMap<>();
private static ButtonState generateNextState(boolean buttonInput, ButtonState oldState) {
switch (oldState) {
case OFF:
if (buttonInput) {
return ButtonState.PRESSED;
} else {
return ButtonState.OFF;
}
case PRESSED:
if (buttonInput) {
return ButtonState.HELD;
} else {
return ButtonState.RELEASED;
}
case HELD:
if (buttonInput) {
return ButtonState.HELD;
} else {
return ButtonState.RELEASED;
}
case RELEASED:
if (buttonInput) {
return ButtonState.PRESSED;
} else {
return ButtonState.OFF;
}
default:
return ButtonState.OFF;
}
}
public EventContainer addHandler(ButtonState event, EventListener listener) {
mEventHandlers.put(event, listener);
return this;
}
public EventContainer handle() {
if (mEventHandlers.containsKey(mState)) {
mEventHandlers.get(mState).onEvent();
}
return this;
}
public EventContainer nextState(boolean buttonInput) {
mState = generateNextState(buttonInput, mState);
return this;
}
}
ButtonState.java: This enum contains the four states that a button can be in.
package org.firstinspires.ftc.teamcode.teleop;
public enum ButtonState {
PRESSED, // Button was just pressed
HELD, // The button has been pressed for more than one iteration
RELEASED, // The button was just released
OFF // The button has been released for more than one iteration
}
EventListener.java: This simple interface is implemented by users and is called when an event occurs.
package org.firstinspires.ftc.teamcode.teleop;
public interface EventListener {
void onEvent();
}
Example usage:
// Initialization
gamepadInstance.on(ButtonState.PRESSED, Button.A, new EventListener() {
@Override
public void onEvent() {
// Somehow react to the 'A' button being pressed
openRobotClaw();
}
});
// ...
// Repeats in a loop as long as the robot runs
gamepadInstance.handleEvents();
My questions
- Is there a better way to handle the extraction of the pressed booleans from instances of
com.qualcomm.robotcore.hardware.Gamepad
? I used the enum method because it ensures that even if a new Button is added, there will still be a way to extract the boolean and without having to update different parts of the code. However, it feels like there is a lot of boilerplate code to perform a simple task. - Are there any ways to simplify my code?
- Is there a better way for users to create event handlers?
- What best practices am I missing here?