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
2 changes: 2 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,8 @@ jobs:
target: esp32s3
- path: 'components/vl53l/example'
target: esp32s3
- path: 'components/vmu-pro/example'
target: esp32s3
- path: 'components/wifi/example'
target: esp32
- path: 'components/wrover-kit/example'
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/upload_components.yml
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@ jobs:
components/tt21100
components/utils
components/vl53l
components/vmu-pro
components/wifi
Comment thread
finger563 marked this conversation as resolved.
components/wrover-kit
components/ws-s3-geek
Expand Down
7 changes: 7 additions & 0 deletions components/vmu-pro/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# only register the component if the target is esp32s3
idf_component_register(
INCLUDE_DIRS "include"
SRC_DIRS "src"
REQUIRES driver esp_driver_i2s fatfs base_component display display_drivers interrupt led spi task
REQUIRED_IDF_TARGETS "esp32s3"
)
23 changes: 23 additions & 0 deletions components/vmu-pro/Kconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
menu "VMU Pro Configuration"
config VMU_PRO_INTERRUPT_STACK_SIZE
int "Interrupt stack size"
default 4096
help
Size of the stack used for the interrupt handler. Shared by all of
the button callbacks.

config VMU_PRO_INTERRUPT_PRIORITY
int "Interrupt task priority"
default 20
range 0 25
help
FreeRTOS priority of the GPIO interrupt handler task (0 = lowest). Raise it
if interrupt callbacks must run promptly relative to other tasks.

config VMU_PRO_INTERRUPT_CORE_ID
int "Interrupt task core ID"
default -1
range -1 1
help
Core to pin the GPIO interrupt handler task to (-1 = not pinned to any core).
endmenu
31 changes: 31 additions & 0 deletions components/vmu-pro/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# VMU Pro Board Support Package (BSP) Component

[![Badge](https://components.espressif.com/components/espp/vmu-pro/badge.svg)](https://components.espressif.com/components/espp/vmu-pro)
Comment thread
finger563 marked this conversation as resolved.

The [VMU Pro](https://8bitmods.com/vmupro-handheld-visual-memory-card-for-dreamcast-classic-white/)
by [8BitMods](https://8bitmods.com) is an ESP32-S3 based replacement for the
Sega Dreamcast Visual Memory Unit (VMU) which doubles as a standalone handheld
gaming device. It features:

* 1.5" 240x240 IPS TFT color display
* D-pad, A, B, Mode, Power, and Bottom buttons
* Mono speaker (I2S amplifier)
* micro-SD card slot (mounted at `/sdcard`)
* Rechargeable battery with USB-C charging

The `espp::VmuPro` component provides a singleton hardware abstraction for
initializing the display, buttons, audio, and uSD card subsystems.

> **Warning:** The GPIO assignments in this component are UNVERIFIED
> placeholders. The VMU Pro's schematic is not publicly available, so the pin
> numbers in `include/vmu-pro.hpp` must be corrected against real hardware or
> vendor documentation before the component will function on the device. The
> peripheral set itself (display size and color format, button list, audio
> capabilities, uSD card) comes from the official
> [VMU Pro SDK](https://github.com/AppCakeLtd/vmupro-sdk) and is accurate.

## Example

The [example](./example) shows how to use the `espp::VmuPro` hardware
abstraction component to initialize the components on the VMU Pro and build a
simple LVGL UI (via a `Gui` class) driven by the buttons.
22 changes: 22 additions & 0 deletions components/vmu-pro/example/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# The following lines of boilerplate have to be in your project's CMakeLists
# in this exact order for cmake to work correctly
cmake_minimum_required(VERSION 3.20)

set(ENV{IDF_COMPONENT_MANAGER} "0")
include($ENV{IDF_PATH}/tools/cmake/project.cmake)

# add the component directories that we want to use
set(EXTRA_COMPONENT_DIRS
"../../../components/"
)

set(
COMPONENTS
"main esptool_py vmu-pro"
CACHE STRING
"List of components to include"
)

project(vmu_pro_example)

set(CMAKE_CXX_STANDARD 20)
41 changes: 41 additions & 0 deletions components/vmu-pro/example/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# VMU Pro Example

This example shows how to use the `espp::VmuPro` hardware abstraction component
to initialize the components on the [8BitMods VMU
Pro](https://8bitmods.com/vmupro-handheld-visual-memory-card-for-dreamcast-classic-white/).

It initializes the display, buttons, audio, and uSD card subsystems and builds
a simple LVGL UI using a `Gui` class:

- The D-pad moves a cursor around the screen
- The A button draws a circle at the cursor (and plays a beep)
- The B button clears the circles (and plays a lower beep)
- The Mode button rotates the display through 0/90/180/270 degrees
- The Bottom button cycles the backlight brightness (25/50/75/100%)
- The Power button toggles the audio mute

> **Warning:** The GPIO assignments in the `espp::VmuPro` component are
> UNVERIFIED placeholders (the VMU Pro's schematic is not publicly available),
> so this example will build but is not expected to function on real hardware
> until the pins in `components/vmu-pro/include/vmu-pro.hpp` are corrected.

## How to use example

### Hardware Required

This example is designed to run on the 8BitMods VMU Pro.

### Build and Flash

Build the project and flash it to the board, then run monitor tool to view
serial output:

```
idf.py -p PORT flash monitor
```

(Replace PORT with the name of the serial port to use.)

(To exit the serial monitor, type ``Ctrl-]``.)

See the Getting Started Guide for full steps to configure and use ESP-IDF to build projects.
1 change: 1 addition & 0 deletions components/vmu-pro/example/main/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
idf_component_register(SRC_DIRS "." INCLUDE_DIRS ".")
212 changes: 212 additions & 0 deletions components/vmu-pro/example/main/gui.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
#include "gui.hpp"

void Gui::init_ui() {
std::lock_guard<std::recursive_mutex> lock(mutex_);
init_background();
init_label();
init_circle_layer();
init_cursor();
// start with the cursor in the center of the screen
auto &vmu = espp::VmuPro::get();
cursor_x_ = vmu.rotated_display_width() / 2;
cursor_y_ = vmu.rotated_display_height() / 2;
update_cursor();
// disable scrolling on the screen
lv_obj_set_scrollbar_mode(lv_screen_active(), LV_SCROLLBAR_MODE_OFF);
lv_obj_clear_flag(lv_screen_active(), LV_OBJ_FLAG_SCROLLABLE);
}

void Gui::deinit_ui() {
std::lock_guard<std::recursive_mutex> lock(mutex_);
lv_obj_clean(lv_screen_active());
}

void Gui::init_background() {
auto &vmu = espp::VmuPro::get();
background_ = lv_obj_create(lv_screen_active());
lv_obj_set_size(background_, vmu.lcd_width(), vmu.lcd_height());
lv_obj_set_style_bg_color(background_, lv_color_make(0, 0, 0), 0);
}

void Gui::init_label() {
label_ = lv_label_create(lv_screen_active());
lv_label_set_text(label_, "");
lv_obj_align(label_, LV_ALIGN_TOP_LEFT, 0, 0);
lv_obj_set_style_text_align(label_, LV_TEXT_ALIGN_LEFT, 0);
}

void Gui::init_cursor() {
cursor_ = lv_obj_create(lv_screen_active());
lv_obj_remove_style_all(cursor_);
lv_obj_set_size(cursor_, CURSOR_RADIUS * 2, CURSOR_RADIUS * 2);
lv_obj_set_style_radius(cursor_, LV_RADIUS_CIRCLE, 0);
lv_obj_set_style_border_width(cursor_, 2, 0);
lv_obj_set_style_border_color(cursor_, lv_palette_main(LV_PALETTE_YELLOW), 0);
lv_obj_set_style_bg_opa(cursor_, LV_OPA_TRANSP, 0);
lv_obj_clear_flag(cursor_, LV_OBJ_FLAG_CLICKABLE);
lv_obj_clear_flag(cursor_, LV_OBJ_FLAG_SCROLLABLE);
}

void Gui::init_circle_layer() {
auto &vmu = espp::VmuPro::get();
circle_layer_ = lv_obj_create(lv_screen_active());
lv_obj_remove_style_all(circle_layer_);
lv_obj_set_size(circle_layer_, vmu.lcd_width(), vmu.lcd_height());
lv_obj_align(circle_layer_, LV_ALIGN_CENTER, 0, 0);
lv_obj_clear_flag(circle_layer_, LV_OBJ_FLAG_CLICKABLE);
lv_obj_clear_flag(circle_layer_, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_set_style_bg_opa(circle_layer_, LV_OPA_TRANSP, 0);
lv_obj_set_style_border_width(circle_layer_, 0, 0);
lv_obj_set_style_outline_width(circle_layer_, 0, 0);
lv_obj_set_style_shadow_width(circle_layer_, 0, 0);
lv_obj_add_event_cb(circle_layer_, draw_circle_layer, LV_EVENT_DRAW_MAIN, this);
}

bool Gui::update(std::mutex &m, std::condition_variable &cv) {
{
std::lock_guard<std::recursive_mutex> lock(mutex_);
lv_task_handler();
}
std::unique_lock<std::mutex> lock(m);
cv.wait_for(lock, std::chrono::milliseconds(16));
return false; // don't stop the task
}

void Gui::set_label_text(std::string_view text) {
std::lock_guard<std::recursive_mutex> lock(mutex_);
lv_label_set_text(label_, std::string(text).c_str());
}

void Gui::move_cursor(int dx, int dy) {
std::lock_guard<std::recursive_mutex> lock(mutex_);
auto &vmu = espp::VmuPro::get();
int width = vmu.rotated_display_width();
int height = vmu.rotated_display_height();
cursor_x_ = std::clamp(cursor_x_ + dx, CURSOR_RADIUS, width - CURSOR_RADIUS);
cursor_y_ = std::clamp(cursor_y_ + dy, CURSOR_RADIUS, height - CURSOR_RADIUS);
update_cursor();
}

void Gui::update_cursor() {
lv_obj_set_pos(cursor_, cursor_x_ - CURSOR_RADIUS, cursor_y_ - CURSOR_RADIUS);
lv_obj_move_foreground(cursor_);
}

void Gui::draw_at_cursor() {
std::lock_guard<std::recursive_mutex> lock(mutex_);
draw_circle_impl(cursor_x_, cursor_y_, CIRCLE_RADIUS);
}

void Gui::next_rotation() {
std::lock_guard<std::recursive_mutex> lock(mutex_);
auto &vmu = espp::VmuPro::get();
clear_circles_impl();
auto rotation = lv_display_get_rotation(lv_display_get_default());
rotation = static_cast<lv_display_rotation_t>((static_cast<int>(rotation) + 1) % 4);
lv_display_set_rotation(lv_display_get_default(), rotation);
// update the size of the screen-filling objects
lv_obj_set_size(background_, vmu.rotated_display_width(), vmu.rotated_display_height());
lv_obj_set_size(circle_layer_, vmu.rotated_display_width(), vmu.rotated_display_height());
lv_obj_align(circle_layer_, LV_ALIGN_CENTER, 0, 0);
lv_obj_invalidate(circle_layer_);
// re-center the cursor within the (possibly differently-sized) screen
cursor_x_ = vmu.rotated_display_width() / 2;
cursor_y_ = vmu.rotated_display_height() / 2;
update_cursor();
}

void Gui::cycle_brightness() {
std::lock_guard<std::recursive_mutex> lock(mutex_);
brightness_index_ = (brightness_index_ + 1) % BRIGHTNESS_LEVELS.size();
float new_brightness = BRIGHTNESS_LEVELS[brightness_index_];
espp::VmuPro::get().brightness(new_brightness);
logger_.info("Set brightness to {:.0f}%", new_brightness);
}

void Gui::draw_circle_impl(int x, int y, int radius) {
lv_obj_move_foreground(circle_layer_);
lv_obj_move_foreground(cursor_);
Circle previous_circle = circles_[next_circle_index_];
circles_[next_circle_index_] = {.x = x, .y = y, .radius = radius, .visible = true};
next_circle_index_ = (next_circle_index_ + 1) % circles_.size();
if (visible_circle_count_ < circles_.size()) {
visible_circle_count_++;
}
if (previous_circle.visible) {
invalidate_circle_area(previous_circle);
}
invalidate_circle_area(circles_[(next_circle_index_ + circles_.size() - 1) % circles_.size()]);
}

void Gui::clear_circles() {
std::lock_guard<std::recursive_mutex> lock(mutex_);
clear_circles_impl();
}

void Gui::clear_circles_impl() {
for (auto &circle : circles_) {
if (circle.visible) {
invalidate_circle_area(circle);
}
circle.visible = false;
}
next_circle_index_ = 0;
visible_circle_count_ = 0;
}

void Gui::invalidate_circle_area(const Circle &circle) {
if (!circle_layer_ || circle.radius <= 0) {
return;
}
lv_area_t obj_coords;
lv_obj_get_coords(circle_layer_, &obj_coords);
lv_area_t coords = {
.x1 = static_cast<lv_coord_t>(obj_coords.x1 + circle.x - circle.radius),
.y1 = static_cast<lv_coord_t>(obj_coords.y1 + circle.y - circle.radius),
.x2 = static_cast<lv_coord_t>(obj_coords.x1 + circle.x + circle.radius - 1),
.y2 = static_cast<lv_coord_t>(obj_coords.y1 + circle.y + circle.radius - 1),
};
lv_obj_invalidate_area(circle_layer_, &coords);
}

void Gui::draw_circle_layer(lv_event_t *e) {
const auto *gui = static_cast<const Gui *>(lv_event_get_user_data(e));
if (!gui) {
return;
}
gui->draw_circles(e);
}

void Gui::draw_circles(lv_event_t *e) const {
if (visible_circle_count_ == 0) {
return;
}

auto *obj = static_cast<lv_obj_t *>(lv_event_get_current_target(e));
auto *layer = lv_event_get_layer(e);
lv_area_t obj_coords;
lv_obj_get_coords(obj, &obj_coords);

lv_draw_rect_dsc_t rect_dsc;
lv_draw_rect_dsc_init(&rect_dsc);
rect_dsc.base.layer = layer;
rect_dsc.radius = LV_RADIUS_CIRCLE;
rect_dsc.bg_opa = LV_OPA_70;
rect_dsc.bg_color = lv_color_make(0, 255, 255);
rect_dsc.border_width = 0;
rect_dsc.outline_width = 0;
rect_dsc.shadow_width = 0;

for (const auto &circle : circles_) {
if (!circle.visible) {
continue;
}
lv_area_t coords = {
.x1 = static_cast<lv_coord_t>(obj_coords.x1 + circle.x - circle.radius),
.y1 = static_cast<lv_coord_t>(obj_coords.y1 + circle.y - circle.radius),
.x2 = static_cast<lv_coord_t>(obj_coords.x1 + circle.x + circle.radius - 1),
.y2 = static_cast<lv_coord_t>(obj_coords.y1 + circle.y + circle.radius - 1),
};
lv_draw_rect(layer, &rect_dsc, &coords);
}
}
Loading
Loading