diff --git a/include/gamepad/controller.hpp b/include/gamepad/controller.hpp index cd1aebc..1073fa4 100644 --- a/include/gamepad/controller.hpp +++ b/include/gamepad/controller.hpp @@ -1,9 +1,10 @@ #pragma once #include "pros/misc.h" +#include #include - #include "button.hpp" +#include "joystick_transformation.hpp" #include "pros/misc.hpp" namespace gamepad { @@ -45,30 +46,99 @@ class Gamepad { * * @param joystick Which joystick axis to return * + * @return float the value of the joystick, between -1.0 and 1.0. + * * @b Example: * @code {.cpp} * // control a motor with a joystick - * intake.move(gamepad::master[ANALOG_RIGHT_Y]); + * intake.move(gamepad::master[ANALOG_RIGHT_Y] * 127); * @endcode * */ float operator[](pros::controller_analog_e_t joystick); - 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_A}; - const float& LeftX = m_LeftX; - const float& LeftY = m_LeftY; - const float& RightX = m_RightX; - const float& RightY = m_RightY; + + /// The L1 button on the top of the controller. + const Button& buttonL1(); + + /// The L2 button on the top of the controller. + const Button& buttonL2(); + + /// The R1 button on the top of the controller. + const Button& buttonR1(); + + /// The R2 button on the top of the controller. + const Button& buttonR2(); + + /// The up arrow button on the front of the controller. + const Button& buttonUp(); + + /// The down arrow button on the front of the controller. + const Button& buttonDown(); + + /// The left arrow button on the front of the controller. + const Button& buttonLeft(); + + /// The right arrow button on the front of the controller. + const Button& buttonRight(); + + /// The X arrow button on the front of the controller. + const Button& buttonX(); + + /// The B arrow button on the front of the controller. + const Button& buttonB(); + + /// The Y arrow button on the front of the controller. + const Button& buttonY(); + + /// The A arrow button on the front of the controller. + const Button& buttonA(); + + /** + * @brief Gets the value of the left joystick's x axis, optionally applying a curve. + * + * @param use_curve (optional) Whether or not to use the curve; defaults to true. + * @return float The value of the left joystick's x-axis, between -1.0 and 1.0. + */ + float axisLeftX(bool use_curve = true); + + /** + * @brief Gets the value of the left joystick's y axis, optionally applying a curve. + * + * @param use_curve (optional) Whether or not to use the curve; defaults to true. + * @return float The value of the left joystick's y-axis, between -1.0 and 1.0. + */ + float axisLeftY(bool use_curve = true); + + /** + * @brief Gets the value of the right joystick's x axis, optionally applying a curve. + * + * @param use_curve (optional) Whether or not to use the curve; defaults to true. + * @return float The value of the right joystick's x-axis, between -1.0 and 1.0. + */ + float axisRightX(bool use_curve = true); + + /** + * @brief Gets the value of the right joystick's y axis, optionally applying a curve. + * + * @param use_curve (optional) Whether or not to use the curve; defaults to true. + * @return float The value of the right joystick's y-axis, between -1.0 and 1.0. + */ + float axisRightY(bool use_curve = true); + + /** + * @brief Set the transformation to be used for the left joystick. + * + * @param left_transformation The transformation to be used + */ + void set_left_transform(Transformation left_transformation); + + /** + * @brief Set the transformation to be used for the right joystick. + * + * @param right_transformation The transformation to be used + */ + void set_right_transform(Transformation right_transformation); + /// The master controller, same as @ref gamepad::master static Gamepad master; /// The partner controller, same as @ref gamepad::partner @@ -81,6 +151,8 @@ class Gamepad { m_A {}; float m_LeftX = 0, m_LeftY = 0, m_RightX = 0, m_RightY = 0; Button Fake {}; + std::optional m_left_transformation {std::nullopt}; + std::optional m_right_transformation {std::nullopt}; /** * @brief Gets a unique name for a listener that will not conflict with user listener names. * diff --git a/include/gamepad/joystick_transformation.hpp b/include/gamepad/joystick_transformation.hpp new file mode 100644 index 0000000..6f028db --- /dev/null +++ b/include/gamepad/joystick_transformation.hpp @@ -0,0 +1,190 @@ +#pragma once + +#include +#include +#include +#include + +namespace gamepad { + +/** + * @brief An abstract class for joystick transformations. + * + * A transformation takes a coordinate representing the value of the joystick, and returns a transformed coordinate + * value + * + */ +class AbstractTransformation { + public: + /** + * @brief Get the transformed coordinate given the original. + * + * @param original The original value of the joystick + * @return std::pair The transformed value + */ + virtual std::pair get_value(std::pair original) = 0; + virtual ~AbstractTransformation() = default; +}; + +/** + * @brief A joystick transformation that applies a deadband to the joystick values + * + * A deadband makes the joystick value zero when the value is close to zero. This helps prevent drifting, since + * joysticks often do not read exactly zero when released. + */ +class Deadband : public AbstractTransformation { + public: + /** + * @brief Construct a new Deadband object + * + * @param x_deadband The deadband to apply for the x axis. + * @param y_deadband The deadband to apply for the x axis. + * @param x_spread How much the deadband for the x axis should widen. + * @param y_spread How much the deadband for the y axis should widen. + */ + Deadband(float x_deadband, float y_deadband, float x_spread, float y_spread) + : m_x_deadband(x_deadband), + m_y_deadband(y_deadband), + m_x_spread(x_spread), + m_y_spread(y_spread) {} + + /** + * @brief Construct a new Deadband object + * + * @param x_deadband The deadband to apply for the x axis. + * @param y_deadband The deadband to apply for the y axis. + */ + Deadband(float x_deadband, float y_deadband) + : Deadband(x_deadband, y_deadband, 0.0, 0.0) {} + + /** + * @brief Get the joystick coordinate after applying the deadband + * + * @param original The value of the joystick before applying the deadband + * @return std::pair The joystick coordinate, with a deadband applied + */ + std::pair get_value(std::pair original) override; + private: + float m_x_deadband; + float m_y_deadband; + float m_x_spread; + float m_y_spread; +}; + +/** + * @brief A joystick transformation that applies an expo curve to the joystick values + * + * An expo curve allows greater control of the joystick, by reducing the joystick values at low speeds, while still + * allowing you to attain the maximum value of the joysticks. + */ +class ExpoCurve : public AbstractTransformation { + public: + /** + * @brief Construct a new Expo Curve object + * + * @param x_curve How much the x axis should be curved. A higher value curves the joystick value more. + * @param y_curve How much the y axis should be curved. A higher value curves the joystick value more. + */ + ExpoCurve(float x_curve, float y_curve) + : m_x_curve(x_curve), + m_y_curve(y_curve) {} + + /** + * @brief Get the joystick coordinate after applying the curve + * + * @param original The value of the joystick before applying the curve + * @return std::pair The joystick coordinate, with a curve applied + */ + std::pair get_value(std::pair original) override; + private: + float m_x_curve; + float m_y_curve; +}; + +/** + * @brief A joystick transformation that applies a fisheye to the joystick values + * + * The vex controller joysticks don't reach their maximum value in the corners. This can be an issue, especially when + * using single stick arcade. The fisheye "stretches" the joystick values so that they attain their maximum value even + * in the corners of the joysticks. + */ +class Fisheye : public AbstractTransformation { + public: + /** + * @brief Construct a new Fisheye object + * + * @param radius The radius of the rounded circle that forms the corners of the joystick's housing. + */ + Fisheye(float radius) + : m_radius(radius) {} + + /** + * @brief Get the joystick coordinate after applying the fisheye + * + * @param original The value of the joystick before applying the fisheye + * @return std::pair The joystick coordinate, with a fisheye applied + */ + std::pair get_value(std::pair original) override; + private: + float m_radius; +}; + +/** + * @brief A chain of transformations. This class should not be directly used, but should be constructed using the + * TransformationBuilder class. + */ +class Transformation final { + friend class TransformationBuilder; + public: + std::pair get_value(std::pair); + private: + std::vector> m_all_transforms; +}; + +/** + * @brief A class to create a chain of transformations. + * + */ +class TransformationBuilder final { + public: + /** + * @brief Construct a new Transformation Builder object + * + * @param first The transformation that should be used first + */ + template T> TransformationBuilder(T first) { + m_transform.m_all_transforms.push_back(std::make_unique(std::move(first))); + } + + TransformationBuilder() = delete; + + /** + * @brief Add a transformation to the list of transformations to be applied. + * + * @param next The next transformation to be applied after the previous specified transformation + * @return TransformationBuilder& The original Transformation Builder. + */ + template T> TransformationBuilder& and_then(T next) { + m_transform.m_all_transforms.push_back(std::make_unique(std::move(next))); + return *this; + } + + /** + * @brief Generate the final chained transformation + * + * @return Transformation The final chained transformation. This can be passed to + * set_left_transform/set_right_transform + */ + Transformation build() { return std::move(m_transform); } + + /** + * @brief Generate the final chained transformation + * + * @return Transformation The final chained transformation. This can be passed to + * set_left_transform/set_right_transform + */ + operator Transformation() { return std::move(m_transform); } + private: + Transformation m_transform {}; +}; +} // namespace gamepad diff --git a/src/gamepad/controller.cpp b/src/gamepad/controller.cpp index fff75f2..8396988 100644 --- a/src/gamepad/controller.cpp +++ b/src/gamepad/controller.cpp @@ -16,20 +16,20 @@ void Gamepad::update() { this->updateButton(static_cast(i)); } - this->m_LeftX = this->controller.get_analog(pros::E_CONTROLLER_ANALOG_LEFT_X); - this->m_LeftY = this->controller.get_analog(pros::E_CONTROLLER_ANALOG_LEFT_Y); - this->m_RightX = this->controller.get_analog(pros::E_CONTROLLER_ANALOG_RIGHT_X); - this->m_RightY = this->controller.get_analog(pros::E_CONTROLLER_ANALOG_RIGHT_Y); + this->m_LeftX = this->controller.get_analog(pros::E_CONTROLLER_ANALOG_LEFT_X) / 127.0; + this->m_LeftY = this->controller.get_analog(pros::E_CONTROLLER_ANALOG_LEFT_Y) / 127.0; + this->m_RightX = this->controller.get_analog(pros::E_CONTROLLER_ANALOG_RIGHT_X) / 127.0; + this->m_RightY = this->controller.get_analog(pros::E_CONTROLLER_ANALOG_RIGHT_Y) / 127.0; } const Button& Gamepad::operator[](pros::controller_digital_e_t button) { return this->*Gamepad::button_to_ptr(button); } float Gamepad::operator[](pros::controller_analog_e_t axis) { switch (axis) { - case pros::E_CONTROLLER_ANALOG_LEFT_X: return this->LeftX; - case pros::E_CONTROLLER_ANALOG_LEFT_Y: return this->LeftY; - case pros::E_CONTROLLER_ANALOG_RIGHT_X: return this->RightX; - case pros::E_CONTROLLER_ANALOG_RIGHT_Y: return this->RightY; + case pros::E_CONTROLLER_ANALOG_LEFT_X: return m_LeftX; + case pros::E_CONTROLLER_ANALOG_LEFT_Y: return m_LeftY; + case pros::E_CONTROLLER_ANALOG_RIGHT_X: return m_RightX; + case pros::E_CONTROLLER_ANALOG_RIGHT_Y: return m_RightY; default: TODO("add error logging") errno = EINVAL; @@ -42,6 +42,58 @@ std::string Gamepad::unique_name() { return std::to_string(i++) + "_internal"; } +const Button& Gamepad::buttonL1() { return m_L1; } + +const Button& Gamepad::buttonL2() { return m_L2; } + +const Button& Gamepad::buttonR1() { return m_R1; } + +const Button& Gamepad::buttonR2() { return m_R2; } + +const Button& Gamepad::buttonUp() { return m_Up; } + +const Button& Gamepad::buttonDown() { return m_Down; } + +const Button& Gamepad::buttonLeft() { return m_Left; } + +const Button& Gamepad::buttonRight() { return m_Right; } + +const Button& Gamepad::buttonX() { return m_X; } + +const Button& Gamepad::buttonB() { return m_B; } + +const Button& Gamepad::buttonY() { return m_Y; } + +const Button& Gamepad::buttonA() { return m_A; } + +float Gamepad::axisLeftX(bool use_curve) { + if (use_curve && m_left_transformation) return m_left_transformation->get_value({m_LeftX, m_LeftY}).first; + else return m_LeftX; +} + +float Gamepad::axisLeftY(bool use_curve) { + if (use_curve && m_left_transformation) return m_left_transformation->get_value({m_LeftX, m_LeftY}).second; + else return m_LeftY; +} + +float Gamepad::axisRightX(bool use_curve) { + if (use_curve && m_right_transformation) return m_right_transformation->get_value({m_RightX, m_RightY}).first; + else return m_RightX; +} + +float Gamepad::axisRightY(bool use_curve) { + if (use_curve && m_right_transformation) return m_right_transformation->get_value({m_RightX, m_RightY}).second; + else return m_RightY; +} + +void Gamepad::set_left_transform(Transformation left_transformation) { + m_left_transformation = std::move(left_transformation); +} + +void Gamepad::set_right_transform(Transformation right_transformation) { + m_right_transformation = std::move(right_transformation); +} + Button Gamepad::*Gamepad::button_to_ptr(pros::controller_digital_e_t button) { switch (button) { case pros::E_CONTROLLER_DIGITAL_L1: return &Gamepad::m_L1; diff --git a/src/gamepad/joystick_transformation.cpp b/src/gamepad/joystick_transformation.cpp new file mode 100644 index 0000000..225c2d1 --- /dev/null +++ b/src/gamepad/joystick_transformation.cpp @@ -0,0 +1,50 @@ +#include "joystick_transformation.hpp" +#include +#include + +using std::abs; +using std::copysign; +using std::pow; + +namespace gamepad { +std::pair Deadband::get_value(std::pair value) { + float x = value.first; + float y = value.second; + float x_deadband = m_x_deadband + abs(y) * m_x_spread; + float y_deadband = m_y_deadband + abs(x) * m_y_spread; + float x_scale = 1.0 / (1.0 - x_deadband); + float y_scale = 1.0 / (1.0 - y_deadband); + x = copysign(abs(x) < x_deadband ? 0 : (abs(x) - x_deadband) * x_scale, x); + y = copysign(abs(y) < y_deadband ? 0 : (abs(y) - y_deadband) * y_scale, y); + return {x, y}; +} + +std::pair ExpoCurve::get_value(std::pair value) { + float x = value.first; + float y = value.second; + x = copysign(pow(abs(x), m_x_curve), x); + y = copysign(pow(abs(y), m_y_curve), y); + return {x, y}; +} + +std::pair Fisheye::get_value(std::pair value) { + float x = value.first; + float y = value.second; + float x_abs = abs(x); + float y_abs = abs(y); + float j = std::sqrt(m_radius * m_radius - 1.0 * 1.0); + if (x_abs >= j && y_abs >= j) { + float theta = std::atan2(y_abs, x_abs); + x_abs *= m_radius / std::cos(abs(std::remainder(theta, 90))); + y_abs *= m_radius / std::cos(abs(std::remainder(theta, 90))); + } + x = std::copysign(std::min(1.0f, x_abs), x); + y = std::copysign(std::min(1.0f, y_abs), y); + return {x, y}; +} + +std::pair Transformation::get_value(std::pair value) { + return std::accumulate(m_all_transforms.begin(), m_all_transforms.end(), value, + [](auto last_val, auto& next_transform) { return next_transform->get_value(last_val); }); +} +} // namespace gamepad \ No newline at end of file diff --git a/src/main.cpp b/src/main.cpp index b0bef76..0b00a45 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,7 +1,5 @@ #include "main.h" #include "gamepad/api.hpp" -#include "gamepad/controller.hpp" -#include "pros/rtos.hpp" #include #include @@ -36,22 +34,26 @@ void aRepeatPress1() { void initialize() { // We can register functions to run when buttons are pressed - gamepad::master.Down.onPress("downPress1", downPress1); + gamepad::master.buttonDown().onPress("downPress1", downPress1); // ...or when they're released - gamepad::master.Up.onRelease("downRelease1", upRelease1); + gamepad::master.buttonUp().onRelease("downRelease1", upRelease1); // There's also the longPress event - gamepad::master.Left.onLongPress("leftLongPress1", leftLongPress1); + gamepad::master.buttonLeft().onLongPress("leftLongPress1", leftLongPress1); // We can have two or even more functions on one button, // just remember to give them different names - gamepad::master.Left.onShortRelease("leftShortRelease", leftShortRelease1); - gamepad::master.Left.onLongRelease("leftLongRelease", leftLongRelease1); + gamepad::master.buttonLeft().onShortRelease("leftShortRelease", leftShortRelease1); + gamepad::master.buttonLeft().onLongRelease("leftLongRelease", leftLongRelease1); // We also have the repeat press event, where we can adjust the timing - gamepad::master.A.set_long_press_threshold(1000); // in ms - gamepad::master.A.set_repeat_cooldown(100); // in ms - gamepad::master.A.onPress("aStartPress", aPress1); - gamepad::master.A.onRepeatPress("aRepeatPress", aRepeatPress1); + gamepad::master.buttonA().set_long_press_threshold(1000); // in ms + gamepad::master.buttonA().set_repeat_cooldown(100); // in ms + gamepad::master.buttonA().onPress("aStartPress", aPress1); + gamepad::master.buttonA().onRepeatPress("aRepeatPress", aRepeatPress1); // And we can use lambda's too - gamepad::master.X.onShortRelease("xShortRelease1", []() { printf("X Short Release!\n"); }); + gamepad::master.buttonX().onShortRelease("xShortRelease1", []() { printf("X Short Release!\n"); }); + + // set up controller curves: + gamepad::master.set_left_transform( + gamepad::TransformationBuilder(gamepad::Deadband(0.05, 0.05)).and_then(gamepad::ExpoCurve(2, 2))); } /** @@ -106,8 +108,8 @@ void opcontrol() { // Remember to ALWAYS call update at the start of your while loop! gamepad::master.update(); // We'll use the arcade control scheme - int dir = gamepad::master.LeftY; // Gets amount forward/backward from left joystick - int turn = gamepad::master.RightX; // Gets the turn left/right from right joystick + int dir = gamepad::master.axisLeftY() * 127; // Gets amount forward/backward from left joystick + int turn = gamepad::master.axisRightX() * 127; // Gets the turn left/right from right joystick left_mg.move(dir - turn); // Sets left motor voltage right_mg.move(dir + turn); // Sets right motor voltage pros::delay(25); // Wait for 25 ms, then update the motor values again