From 4a9bf5ed4cf7bfab00c4d58983c56a4b1a4c2358 Mon Sep 17 00:00:00 2001 From: Andrew Curtis <80860310+meisZWFLZ@users.noreply.github.com> Date: Wed, 11 Jun 2025 21:27:23 -0700 Subject: [PATCH 1/3] feat: :construction: create outline for screen --- include/pros/devices/brain.hpp | 3 + include/pros/devices/screen.hpp | 110 ++++++++++++++++++++++++++++++++ src/devices/screen.cpp | 4 ++ 3 files changed, 117 insertions(+) create mode 100644 include/pros/devices/screen.hpp create mode 100644 src/devices/screen.cpp 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..d7d6cae2 --- /dev/null +++ b/include/pros/devices/screen.hpp @@ -0,0 +1,110 @@ +#pragma once + +#include "stdint.h" + +#include + +namespace zest { + +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. */ + Release, + /** The screen has been touched. */ + Press, + /** The screen has been touched and is still being held. */ + Held + }; + + /** The y coordinate 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 begins to be pressed. + * + * Spawn a new task to check for events if it is not already running. + * All listeners are called from this task. + * + * @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 is pressed. + */ + static void on_pressed(std::function listener_cb); + + /** + * @brief Subscribes the listener to be called when the screen begins to be released. + * + * Spawn a new task to check for events if it is not already running. + * All listeners are called from this task. + * + * @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 is pressed. + */ + static void on_released(std::function listener_cb); + + // TODO: Should there be a on_held() function? + + static void set_render_mode(); + static void get_render_mode(); + 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(); +}; + +} // 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..bf7c1a80 --- /dev/null +++ b/src/devices/screen.cpp @@ -0,0 +1,4 @@ +#include "pros/devices/screen.hpp" + +#include "v5_api_patched.h" +#include "v5_apitypes_patched.h" \ No newline at end of file From 8cb9027d5ec1fbd67239ba2b4945728c04e517a9 Mon Sep 17 00:00:00 2001 From: Andrew Curtis <80860310+meisZWFLZ@users.noreply.github.com> Date: Sat, 14 Jun 2025 21:31:06 -0700 Subject: [PATCH 2/3] feat: :sparkles: add touch listeners and wip render mode --- include/pros/devices/screen.hpp | 94 ++++++++++++++++++++++++++------ src/devices/screen.cpp | 97 ++++++++++++++++++++++++++++++++- 2 files changed, 172 insertions(+), 19 deletions(-) diff --git a/include/pros/devices/screen.hpp b/include/pros/devices/screen.hpp index d7d6cae2..40eedc91 100644 --- a/include/pros/devices/screen.hpp +++ b/include/pros/devices/screen.hpp @@ -1,11 +1,18 @@ #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; @@ -20,14 +27,14 @@ struct Screen { struct TouchEvent { enum class State { /** The screen has been released. */ - Release, + Released, /** The screen has been touched. */ - Press, + Pressed, /** The screen has been touched and is still being held. */ Held }; - /** The y coordinate of the touch. */ + /** The current state of the touch. */ State state; /** The x coordinate of the touch. */ int16_t x; @@ -51,40 +58,87 @@ struct Screen { static TouchEvent get_last_touch(); /** - * @brief Subscribes the listener to be called when the screen begins to be pressed. + * @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 is pressed. + * @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. - * - * Spawn a new task to check for events if it is not already running. - * All listeners are called from this task. - * - * @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 is pressed. + * 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); - // TODO: Should there be a on_held() function? + /** + * @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(); - static void set_render_mode(); - static void 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(); @@ -105,6 +159,10 @@ struct Screen { 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 index bf7c1a80..6361d0b2 100644 --- a/src/devices/screen.cpp +++ b/src/devices/screen.cpp @@ -1,4 +1,99 @@ #include "pros/devices/screen.hpp" +#include "pros/rtos.hpp" #include "v5_api_patched.h" -#include "v5_apitypes_patched.h" \ No newline at end of file +#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 size_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 From 59398df615d42a324fa468ef1525244be0d0e6a1 Mon Sep 17 00:00:00 2001 From: Andrew Curtis <80860310+meisZWFLZ@users.noreply.github.com> Date: Sat, 14 Jun 2025 21:49:45 -0700 Subject: [PATCH 3/3] fix: :rotating_light: fix type of prev_time --- src/devices/screen.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/devices/screen.cpp b/src/devices/screen.cpp index 6361d0b2..ae56bf1f 100644 --- a/src/devices/screen.cpp +++ b/src/devices/screen.cpp @@ -40,11 +40,11 @@ void Screen::on_touched( // 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(); + while (true) { + const auto touch = Screen::get_last_touch(); const auto state = touch.state; - static Screen::TouchEvent::State prev_state = state; - static size_t prev_time = pros::millis(); + static Screen::TouchEvent::State prev_state = state; + static uint32_t prev_time = pros::millis(); if (prev_state != state) for (const auto& listener : listeners)