Skip to content

Commit

Permalink
Merge pull request #6 from LemLib/feature/event-handler
Browse files Browse the repository at this point in the history
feat: ✨ Finish event handler implementation
  • Loading branch information
ion098 authored Sep 6, 2024
2 parents 9c1e994 + 6f1d792 commit 8280f2f
Show file tree
Hide file tree
Showing 5 changed files with 424 additions and 114 deletions.
231 changes: 208 additions & 23 deletions include/gamepad/controller.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
#include "pros/misc.h"
#include <cstdint>
#include <functional>
#include <string>
#ifndef PROS_USE_SIMPLE_NAMES
#define PROS_USE_SIMPLE_NAMES
#endif
Expand All @@ -17,63 +18,247 @@ enum EventType {
ON_PRESS,
ON_LONG_PRESS,
ON_RELEASE,
ON_SHORT_RELEASE,
};

class Button {
friend class Controller;
public:
/// Whether the button has just been pressed
bool rising_edge = false;
/// Whether the button has just been released
bool falling_edge = false;
/// Whether the button is currently held down
bool is_pressed = false;
uint32_t last_press_time = pros::millis();
uint32_t last_release_time = last_press_time;
/// How long the button has been held down
uint32_t time_held = 0;
/// How long the button has been released
uint32_t time_released = 0;
/// How long the threshold should be for the longPress and shortRelease events
uint32_t long_press_threshold = 500;
/**
* @brief Register a function to run when the button is pressed.
*
* @param listenerName The name of the listener, this must be a unique name
* @param func The function to run when the button is pressed, the function MUST NOT block
* @return true The listener was successfully registered
* @return false The listener was not successfully registered (there is already a listener with this name)
*
* @b Example:
* @code {.cpp}
* // Use a function...
* Gamepad::master.Down.onPress("downPress1", downPress1);
* // ...or a lambda
* Gamepad::master.Up.onPress("upPress1", []() { std::cout << "I was pressed!" << std::endl; });
* @endcode
*/
bool onPress(std::string listenerName, std::function<void(void)> func) const;
/**
* @brief Register a function to run when the button is long pressed.
*
* By default, onLongPress will fire when the button has been held down for
* 500ms or more, this threshold can be adjusted by changing long_press_threshold.
*
* @warning When using this event along with onPress, both the onPress
* and onlongPress listeners may fire together.
*
* @param listenerName The name of the listener, this must be a unique name
* @param func The function to run when the button is long pressed, the function MUST NOT block
* @return true The listener was successfully registered
* @return false The listener was not successfully registered (there is already a listener with this name)
*
* @b Example:
* @code {.cpp}
* // Use a function...
* Gamepad::master.Left.onLongPress("fireCatapult", fireCatapult);
* // ...or a lambda
* Gamepad::master.Right.onLongPress("print_right", []() { std::cout << "Right button was long pressed!" <<
* std::endl; });
* @endcode
*/
bool onLongPress(std::string listenerName, std::function<void(void)> func) const;
/**
* @brief Register a function to run when the button is released.
*
* @param listenerName The name of the listener, this must be a unique name
* @param func The function to run when the button is released, the function MUST NOT block
* @return true The listener was successfully registered
* @return false The listener was not successfully registered (there is already a listener with this name)
*
* @b Example:
* @code {.cpp}
* // Use a function...
* Gamepad::master.X.onRelease("stopFlywheel", stopFlywheel);
* // ...or a lambda
* Gamepad::master.Y.onRelease("stopIntake", []() { intake.move(0); });
* @endcode
*/
bool onRelease(std::string listenerName, std::function<void(void)> func) const;
/**
* @brief Register a function to run when the button is short released.
*
* By default, shortRelease will fire when the button has been released before 500ms, this threshold can be
* adjusted by changing long_press_threshold.
*
* @note This event will most likely be used along with the longPress event.
*
* @param listenerName The name of the listener, this must be a unique name
* @param func The function to run when the button is short released, the function MUST NOT block
* @return true The listener was successfully registered
* @return false The listener was not successfully registered (there is already a listener with this name)
*
* @b Example:
* @code {.cpp}
* // Use a function...
* Gamepad::master.A.onShortRelease("raiseLiftOneLevel", raiseLiftOneLevel);
* // ...or a lambda
* Gamepad::master.B.onShortRelease("intakeOnePicce", []() { intake.move_relative(600, 100); });
* @endcode
*/
bool onShortRelease(std::string listenerName, std::function<void(void)> func) const;
/**
* @brief Register a function to run for a given event.
*
* @param event Which event to register the listener on.
* @param listenerName The name of the listener, this must be a unique name
* @param func The function to run for the given event, the function MUST NOT block
* @return true The listener was successfully registered
* @return false The listener was not successfully registered (there is already a listener with this name)
*
* @b Example:
* @code {.cpp}
* // Use a function...
* Gamepad::master.L1.addListener(Gamepad::ON_PRESS, "start_spin", startSpin);
* // ...or a lambda
* Gamepad::master.L1.addListener(Gamepad::ON_RELEASE, "stop_spin", []() { motor1.brake(); });
* @endcode
*/
bool addListener(EventType event, std::string listenerName, std::function<void(void)> func) const;
/**
* @brief Removes a listener from the button
* @warning Usage of this function is discouraged.
*
* @param listenerName The name of the listener to remove
* @return true The specified listener was successfully removed
* @return false The specified listener could not be removed
*
* @b Example:
* @code {.cpp}
* // Add an event listener...
* Gamepad::master.L1.addListener(Gamepad::ON_PRESS, "do_something", doSomething);
* // ...and now get rid of it
* Gamepad::master.L1.removeListener("do_something");
* @endcode
*/
bool removeListener(std::string listenerName) const;

uint32_t onPress(std::function<void(void)> func);
uint32_t onLongPress(std::function<void(void)> func);
uint32_t onRelease(std::function<void(void)> func);
uint32_t addListener(EventType event, std::function<void(void)> func);
bool removeListener(uint32_t id);
/**
* @brief Returns a value indicating whether the button is currently being held.
*
* @return true The button is currently pressed
* @return false The button is not currently pressed
*/
explicit operator bool() const { return is_pressed; }
private:
/**
* @brief Updates the button and runs any event handlers, if necessary
*
* @param is_held Whether or not the button is currently held down
*/
void update(bool is_held);

EventHandler<> onPressEvent;
EventHandler<> onLongPressEvent;
EventHandler<> onReleaseEvent;
/// he last time the update function was called
uint32_t last_update_time = pros::millis();
/// The last time the long press event was fired
uint32_t last_long_press_time = 0;
mutable _impl::EventHandler<std::string> onPressEvent {};
mutable _impl::EventHandler<std::string> onLongPressEvent {};
mutable _impl::EventHandler<std::string> onReleaseEvent {};
mutable _impl::EventHandler<std::string> onShortReleaseEvent {};
};

class Controller {
public:
/**
* Updates the state of the gamepad (all joysticks and buttons), and also runs
* any registered handlers.
* @brief Updates the state of the gamepad (all joysticks and buttons), and also runs
* any registered listeners.
*
* @note This function should be called at the beginning of every loop iteration.
* @note Create a separate instance for each task.
*
* @b Example:
* @code {.cpp}
* while (true) {
* Gamepad::master.update();
* // do robot control stuff here...
* pros::delay(25);
* }
* @endcode
*
*/
void update();
/**
* Get the state of a button on the controller.
* @param button Which button's state you want.
* @brief Get the state of a button on the controller.
*
* @param button Which button to return
*
* @b Example:
* @code {.cpp}
* if(Gamepad::master[DIGITAL_L1]) {
* // do something here...
* }
* @endcode
*
*/
const Button& operator[](pros::controller_digital_e_t button);
/**
* Get the value of a joystick axis on the controller.
* @param joystick Which joystick axis's value to return
* @brief Get the value of a joystick axis on the controller.
*
* @param joystick Which joystick axis to return
*
* @b Example:
* @code {.cpp}
* // control a motor with a joystick
* intake.move(Gamepad::master[ANALOG_RIGHT_Y]);
* @endcode
*
*/
float operator[](pros::controller_analog_e_t joystick);
TODO("hide memebrs and expose getters/const refs")
Button L1 {}, L2 {}, R1 {}, R2 {}, Up {}, Down {}, Left {}, Right {}, X {}, B {}, Y {}, A {};
float LeftX = 0, LeftY = 0, RightX = 0, RightY = 0;
const Button& L1 {m_L1};
const Button& L2 {m_L2};
const Button& R1 {m_R1};
const Button& R2 {m_R2};
const Button& Up {m_Up};
const Button& Down {m_Down};
const Button& Left {m_Left};
const Button& Right {m_Right};
const Button& X {m_X};
const Button& B {m_B};
const Button& Y {m_Y};
const Button& A {m_Down};
const float& LeftX = m_LeftX;
const float& LeftY = m_LeftY;
const float& RightX = m_RightX;
const float& RightY = m_RightY;
/// The master controller, same as @ref Gamepad::master
static Controller master;
/// The partner controller, same as @ref Gamepad::partner
static Controller partner;
private:
explicit Controller(pros::controller_id_e_t id)
Controller(pros::controller_id_e_t id)
: controller(id) {}

Button m_L1 {}, m_L2 {}, m_R1 {}, m_R2 {}, m_Up {}, m_Down {}, m_Left {}, m_Right {}, m_X {}, m_B {}, m_Y {},
m_A {};
float m_LeftX = 0, m_LeftY = 0, m_RightX = 0, m_RightY = 0;
Button Fake {};
/**
* @brief Gets a unique name for a listener that will not conflict with user listener names.
*
* @important: when using the function, you must register the listener by
* directly calling add_listener on the EventHandler, do NOT use onPress/addListener,etc.
*
* @return std::string A unique listener name
*/
static std::string unique_name();
static Button Controller::*button_to_ptr(pros::controller_digital_e_t button);
void updateButton(pros::controller_digital_e_t button_id);
pros::Controller controller;
Expand All @@ -86,4 +271,4 @@ inline Controller& master = Controller::master;
/// The partner controller
inline Controller& partner = Controller::partner;

} // namespace Gamepad
} // namespace Gamepad
84 changes: 54 additions & 30 deletions include/gamepad/event_handler.hpp
Original file line number Diff line number Diff line change
@@ -1,57 +1,81 @@
#pragma once

#include <mutex>
#include <utility>
#include <functional>
#include <map>
#include <atomic>
#include <vector>
#include <algorithm>

#include "gamepad/todo.hpp"
#include "pros/rtos.hpp"
#include "gamepad/recursive_mutex.hpp"

namespace Gamepad {
namespace Gamepad::_impl {

class MonotonicCounter {
template <typename... Args> friend class EventHandler;

static uint32_t next_value() {
static std::atomic<uint32_t> counter = 0;
return ++counter;
}
};

template <typename... Args> class EventHandler {
/**
* @brief Event handling class with thread safety that supports adding, removing, and running listeners
*
* @tparam Key the key type for (un)registering listener (this type MUST support operator== and operator!=)
* @tparam Args the types of the parameters that each listener is passed
*/
template <typename Key, typename... Args> class EventHandler {
public:
using Listener = std::function<void(Args...)>;

uint32_t add_listener(Listener func) {
/**
* @brief Add a listener to the list of listeners
*
* @param key The listener key (this must be a unique key value)
* @param func The function to run when this event is fired
* @return true The listener was successfully added
* @return false The listener was NOT successfully added (there is already a listener with the same key)
*/
bool add_listener(Key key, Listener func) {
std::lock_guard lock(mutex);
uint32_t id = MonotonicCounter::next_value();
listeners.emplace(id, std::move(func));
return id;
if (std::find(keys.begin(), keys.end(), key) != keys.end()) return false;
keys.push_back(key);
listeners.push_back(func);
return true;
}

bool remove_listener(uint32_t id) {
/**
* @brief Remove a listener from the list of listeners
*
* @param key The listener key (this must be a unique key value)
* @return true The listener was successfully removed
* @return false The listener was NOT successfully removed (there is no listener with the same key)
*/
bool remove_listener(Key key) {
std::lock_guard lock(mutex);
if (listeners.find(id) == listeners.end()) {
TODO("change handling maybe?")
return false;
auto i = std::find(keys.begin(), keys.end(), key);
if (i != keys.end()) {
keys.erase(i);
listeners.erase(listeners.begin() + (i - keys.begin()));
return true;
}
listeners.erase(id);
return true;
return false;
}

/**
* @brief Whther or not there are any listeners registered
*
* @return true There are listeners registered
* @return false There are no listeners registered
*/
bool is_empty() {
std::lock_guard lock(mutex);
return listeners.empty();
}

/**
* @brief Runs each listener registered
*
* @param args The parameters to pass to each listener
*/
void fire(Args... args) {
std::lock_guard lock(mutex);
for (auto listener : listeners) { listener.second(args...); }
for (auto listener : listeners) { listener(args...); }
}
private:
std::map<uint32_t, Listener> listeners;
pros::Mutex mutex;
std::vector<Key> keys {};
std::vector<Listener> listeners {};
Gamepad::_impl::RecursiveMutex mutex {};
};
} // namespace Gamepad
} // namespace Gamepad::_impl
Loading

0 comments on commit 8280f2f

Please sign in to comment.