1
\$\begingroup\$

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

  1. 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.
  2. Are there any ways to simplify my code?
  3. Is there a better way for users to create event handlers?
  4. What best practices am I missing here?
\$\endgroup\$
0
\$\begingroup\$

nice job!

Some hints:

Gamepad.java

  • I don't get why do you used same name for Gamepad. Would be better to chose another one (CustomGamepad ?)
  • Both variables can be final since you do not expose setter method. This will help performaces.
  • Since Button is an Enum, you can use EnumMap implementation in place of HashMap. This helps performances and allow you to simplify your code

A good tutorial

private Map<Button, EventContainer> mButtonEvents = new EnumMap<Button, EventContainer>(Button.class);
  • In your constructor, just initialize all EventContainers for-each keys ( check same hint for handleEvents() )

    • public void handleEvents() can be faster. Just check this explanations
  • Since EnumMap has all the keys pre-setted, your method public void on(..) will became smaller since you'll never have null


    public void on(ButtonState event, Button button, EventListener listener) {
//    if (button != null)
    mButtonEvents.get(button).addHandler(event, listener);
    }

Button.java

you simply do not need a private interface and ExtractButton. Simply:

public enum Button {
  A {
    @Override
    public boolean extract(Gamepad gamepad) {
      return gamepad.a;
    }
  },
  B{
    @Override
    public boolean extract(Gamepad gamepad) {
      return gamepad.b;
    }
    // More buttons here
  });


  public abstract boolean extract(Gamepad gamepad);
}
  • of course, if original GamePad has only two buttons (bundle-api methods just return a boolean ?!), this kind of infrastructure maybe it's too much but nevermind.

EventContainer.java

with same "EnumMap" approach, you can have a nice Map to generate state simply with gets avoiding all if-else:


private final Map<ButtonState,Map<Boolean,ButtonState>> buttonStateMachine = new EnumMap<etc.> //it's simple if you use a static initializer

and then your method will look like

 private static ButtonState generateNextState(boolean buttonInput, ButtonState oldState) {
    return buttonStateMachine.get(oldState).get(buttonInput);
 }

however you can use directly your Button object in place of boolean bundle-api object (or avoiding your Button object as said before)

Last, I don't get really how handle() can works in real case scenario but it's more likely a functional issue and not programming one.

\$\endgroup\$
  • \$\begingroup\$ Thank you! This helps a lot, especially your advice about the enums. However, your implementation of the state machine includes a Map with a boolean key. A Map feels overkill because there are only two possible values, especially considering that I didn't see anything like BooleanMap. I'm using a HashMap right now, but the overhead seems like it would be way more than I need. Is there some other data type that would be more appropriate? \$\endgroup\$ – Medude Sep 14 '18 at 20:19
  • \$\begingroup\$ I don't think so. Your main map will be an EnumMap, then you need a key/value system. I don't get why the HashMap should be overkilled (look at constructor of HashMap and initialize all in static way), however it's surely possible to place a List<List<ButtonState>> where 0 is false and 1 is true, but to keep your infrastructure you are able to replace Boolean with your Button item when needed and extends easly \$\endgroup\$ – MrPk Sep 17 '18 at 7:22

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.