diff --git a/include/pros/devices/brain.hpp b/include/pros/devices/brain.hpp index 7b560d1f..ea1003e2 100644 --- a/include/pros/devices/brain.hpp +++ b/include/pros/devices/brain.hpp @@ -1,6 +1,7 @@ #pragma once #include "pros/devices/adi_expander.hpp" +#include "pros/devices/screen.hpp" #include "pros/rtos.hpp" namespace zest { @@ -68,6 +69,8 @@ class Brain { */ static void smart_port_mutex_unlock_all(); + using Screen = zest::Screen; + private: static constinit std::array m_mutexes; }; diff --git a/include/pros/devices/screen.hpp b/include/pros/devices/screen.hpp new file mode 100644 index 00000000..40eedc91 --- /dev/null +++ b/include/pros/devices/screen.hpp @@ -0,0 +1,168 @@ +#pragma once + +#include "pros/rtos.hpp" +#include "stdint.h" + +#include +#include + +namespace zest { + +/** + * @brief A friendlier, documented and safer API wrapper around the Vex SDK's display functions. + * + * @note Heavily inspired by vexide's display module: https://docs.rs/vexide/0.7.0/vexide/devices/display/index.html + */ +struct Screen { + /** Vertical height taken up by the user program in pixels. */ + static constexpr int16_t HEADER_HEIGHT = 32; + /** The width of the screen in pixels. */ + static constexpr int16_t WIDTH = 480; + /** The height of the screen in pixels, excludes the header. */ + static constexpr int16_t HEIGHT = 240; + /** The framerate of the Brain is 60fps. */ + static constexpr int16_t FRAMERATE = 60; + + /** A touch event on the screen. */ + struct TouchEvent { + enum class State { + /** The screen has been released. */ + Released, + /** The screen has been touched. */ + Pressed, + /** The screen has been touched and is still being held. */ + Held + }; + + /** The current state of the touch. */ + State state; + /** The x coordinate of the touch. */ + int16_t x; + /** The y coordinate of the touch. */ + int16_t y; + + // TODO: Determine when it starts counting + /** The number of times the screen has been pressed. */ + int32_t pressCount; + /** The number of times the screen has been released. */ + int32_t releaseCount; + }; + + /** + * @brief Gets the most recent touch event. + * + * TODO: Determine the starting return value (Before the screen is touched). + * + * @return The most recent touch event. + */ + static TouchEvent get_last_touch(); + + /** + * @brief Subscribes the listener to be called when the screen touch state changes. + * + * Spawn a new task to check for events if it is not already running. + * All listeners are called from this task. + * + * TODO: Elaborate on below? + * @warning If you have multiple listeners, avoid using delays, which can delay other callbacks + * from being called, and result in lost information. + * For this reason, it is recommended library developers use get_last_touch() instead of this + * function. + * + * @param listener_cb The function to call when the screen's touch state changes. + * @param state_filter Optional filter for the touch state. If provided, only events matching + * this state will trigger the callback. For example, if state_filter == + * TouchEvent::State::Pressed, then the callback will only be called when the screen starts + * being pressed. + */ + static void on_touched( + std::function listener_cb, + std::optional state_filter = std::nullopt + ); + + /** + * @brief Subscribes the listener to be called when the screen begins to be pressed. + * Wrapper for Screen::on_touched() with state_filter set to TouchEvent::State::Pressed. + * @see Screen::on_touched() + */ + static void on_pressed(std::function listener_cb); + + /** + * @brief Subscribes the listener to be called when the screen begins to be released. + * Wrapper for Screen::on_touched() with state_filter set to TouchEvent::State::Released. + * @see Screen::on_touched() + */ + static void on_released(std::function listener_cb); + + /** + * @brief Subscribes the listener to be called when the screen begins to be held. + * Wrapper for Screen::on_touched() with state_filter set to TouchEvent::State::Held. + * @see Screen::on_touched() + */ + static void on_held(std::function listener_cb); + + /** @brief Determines where screen operations should be written. Immediate is the default. */ + enum class RenderMode { + /** + * Draw operations are immediately applied to the screen without the need to call + * Screen::render(). + * This is the default mode. + */ + Immediate, + /** + * Draw calls are written to an intermediate display buffer, rather than directly drawn to + * the screen. This buffer can later be applied using Screen::render(). + * + * This mode is necessary for preventing screen tearing when drawing at high speeds. + */ + DoubleBuffered, + }; + + /** + * @brief Changes the render mode of the screen. + * @param new_mode The new render mode to set. + * @see Screen::RenderMode + */ + static void set_render_mode(RenderMode new_mode); + + /** + * @brief Gets the current render mode of the screen. + * @return The current render mode of the screen. + * @see Screen::RenderMode + */ + static RenderMode get_render_mode(); + + /** + * @brief Flushes the screen's double buffer it is enabled. + * This does nothing in the immediate render mode, but is required in the immediate render mode. + * @see Screen::RenderMode + */ + static void render(); + + static void scroll(); + static void scroll_region(); + + // TODO: Should these be a single function? see: + // https://docs.rs/vexide/latest/vexide/devices/display/struct.Display.html#method.fill + static void draw_rect(); + static void draw_circle(); + static void draw_line(); + static void draw_pixel(); + + static void fill_rect(); + static void fill_circle(); + + // Should likely use a text object, like vexide does: + // https://docs.rs/vexide/latest/vexide/devices/display/struct.Text.html + static void draw_text(); + + static void clear(auto color /* = TODO*/); + + static void draw_buffer(); + + private: + static RenderMode m_render_mode; + static pros::Mutex m_mutex; +}; + +} // namespace zest \ No newline at end of file diff --git a/src/devices/screen.cpp b/src/devices/screen.cpp new file mode 100644 index 00000000..ae56bf1f --- /dev/null +++ b/src/devices/screen.cpp @@ -0,0 +1,99 @@ +#include "pros/devices/screen.hpp" + +#include "pros/rtos.hpp" +#include "v5_api_patched.h" +#include "v5_apitypes_patched.h" + +#include +#include + +#ifndef ZESTCODE_CONFIG_SCREEN_TOUCH_LISTENER_TASK_INTERVAL + // Default to 16 ms if not defined + #define ZESTCODE_CONFIG_SCREEN_TOUCH_LISTENER_TASK_INTERVAL 1000 / zest::Screen::FRAMERATE +#endif + +#ifndef ZESTCODE_CONFIG_SCREEN_TOUCH_LISTENER_WARNING_THRESHOLD + // Default to 5 ms if not defined + #define ZESTCODE_CONFIG_SCREEN_TOUCH_LISTENER_WARNING_THRESHOLD 5 +#endif +namespace zest { + +void Screen::on_touched( + std::function listener_cb, + std::optional state_filter +) { + struct Listener { + std::function callback; + std::optional filter; + }; + + static std::vector listeners; + listeners.emplace_back(std::move(listener_cb), state_filter); + + // Wait! There's a sdk function that does this for us!: vexTouchUserCallbackSet() + // However, using this would cause the callback to be called from the same task that calls + // vexTasksRun(), the system_daemon task. This is potentially quite dangerous, as the user could + // wait on device data which will never come since the system_daemon task would never reach + // vexTasksRun() again to update the data. + // Instead we use a separate task to check for touch events and call the listeners. + + // TODO: Determine thread safety? (What happens if task A calls this, and while touch_task is + // being constructed, task B preempts it and calls this again?) + static pros::Task touch_task([] { + while (true) { + const auto touch = Screen::get_last_touch(); + const auto state = touch.state; + static Screen::TouchEvent::State prev_state = state; + static uint32_t prev_time = pros::millis(); + + if (prev_state != state) + for (const auto& listener : listeners) + if (!listener.filter.has_value() || listener.filter.value() == state) { + const size_t start_time = pros::millis(); + listener.callback(touch); + + // TODO: Determine the time taken by the actual callback. For example, if + // the callback was preempted mid-way through, that time spend being preempt + // should not count. + const size_t elapsed_time = pros::millis() - start_time; + if (listeners.size() > 1 + && elapsed_time + > ZESTCODE_CONFIG_SCREEN_TOUCH_LISTENER_WARNING_THRESHOLD) { + // TODO: Log a warning if the callback took too long + } + } + prev_state = state; + + // TODO: I recall there being problems with using plain pros::c::task_delay_until with + // lemlib. Do those apply here? + + // Use pros::c::task_delay_until() instead of pros::delay() to avoid drift + pros::c::task_delay_until( + &prev_time, + ZESTCODE_CONFIG_SCREEN_TOUCH_LISTENER_TASK_INTERVAL + ); + } + }); +} + +void Screen::on_pressed(std::function listener_cb) { + on_touched(std::move(listener_cb), TouchEvent::State::Pressed); +} + +void Screen::on_released(std::function listener_cb) { + on_touched(std::move(listener_cb), TouchEvent::State::Released); +} + +void Screen::on_held(std::function listener_cb) { + on_touched(std::move(listener_cb), TouchEvent::State::Held); +} + +void Screen::set_render_mode(Screen::RenderMode new_mode) { + // TODO: Implement +} + +Screen::RenderMode Screen::get_render_mode() { + // TODO: Implement +} + +} // namespace zest \ No newline at end of file