Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions include/pros/devices/brain.hpp
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#pragma once

#include "pros/devices/adi_expander.hpp"
#include "pros/devices/screen.hpp"
#include "pros/rtos.hpp"

namespace zest {
Expand Down Expand Up @@ -68,6 +69,8 @@ class Brain {
*/
static void smart_port_mutex_unlock_all();

using Screen = zest::Screen;

private:
static constinit std::array<pros::RecursiveMutex, 33> m_mutexes;
};
Expand Down
168 changes: 168 additions & 0 deletions include/pros/devices/screen.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
#pragma once

#include "pros/rtos.hpp"
#include "stdint.h"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
#include "stdint.h"
#include <cstdint>


#include <functional>
#include <optional>

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;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are the counts signed integers?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comes from this
May also have to do with vex sdk api, but I'm not sure.
Likely would be good to make unsigned

/** 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.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMO we should either use a separate task for every callback, or not have this functionality built-in.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps. My thinking is that if we have a good mechanism to warn the user at runtime that something bad is going on (via the brain screen), then one task should be good. There was also a large discussion about this in discord and about supporting event listeners for device updates.

*
* 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<void(const TouchEvent&)> listener_cb,
std::optional<TouchEvent::State> 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<void(const TouchEvent&)> 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<void(const TouchEvent&)> 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<void(const TouchEvent&)> 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
99 changes: 99 additions & 0 deletions src/devices/screen.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
#include "pros/devices/screen.hpp"

#include "pros/rtos.hpp"
#include "v5_api_patched.h"
#include "v5_apitypes_patched.h"

#include <utility>
#include <vector>

#ifndef ZESTCODE_CONFIG_SCREEN_TOUCH_LISTENER_TASK_INTERVAL
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the point of this code? Users can't change the #define value without editing ZestCode's source code anyways.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since the user compiles zest code themselves as part of the build process, they can define this macro when calling the compiler thru a cli option.

// 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<void(const TouchEvent&)> listener_cb,
std::optional<Screen::TouchEvent::State> state_filter
) {
struct Listener {
std::function<void(const TouchEvent&)> callback;
std::optional<Screen::TouchEvent::State> filter;
};

static std::vector<Listener> listeners;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This needs a mutex.

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<void(const TouchEvent&)> listener_cb) {
on_touched(std::move(listener_cb), TouchEvent::State::Pressed);
}

void Screen::on_released(std::function<void(const TouchEvent&)> listener_cb) {
on_touched(std::move(listener_cb), TouchEvent::State::Released);
}

void Screen::on_held(std::function<void(const TouchEvent&)> 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
Loading