Skip to content
Merged
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
7 changes: 7 additions & 0 deletions components/i2c/include/i2c.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -578,6 +578,13 @@ class I2c : public BaseComponent {
/// \return The active configuration.
const Config &config() const { return config_; }

/// Get the native ESP-IDF I2C master bus handle.
/// \return The underlying i2c_master_bus_handle_t (nullptr if not initialized).
/// \note Provided so the bus can be shared with ESP-IDF drivers that require
/// the native handle (e.g. camera SCCB via esp_video). The bus remains
/// owned by this object. Only available with the new I2C master API.
i2c_master_bus_handle_t native_bus_handle() const { return master_bus_.native_handle(); }

/// Initialize the I2C bus.
/// \param ec Error code populated on failure.
void init(std::error_code &ec) {
Expand Down
7 changes: 7 additions & 0 deletions components/i2c/include/i2c_master.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,13 @@ class I2cMasterBus : public BaseComponent {
/// @return True if device responds
bool probe(uint16_t device_address, int32_t timeout_ms, std::error_code &ec);

/// @brief Get the native ESP-IDF I2C master bus handle
/// @return The underlying i2c_master_bus_handle_t (nullptr if not initialized)
/// @note This is provided so the bus can be shared with ESP-IDF drivers that
/// require the native handle (e.g. camera SCCB via esp_video). The bus
/// remains owned by this object.
i2c_master_bus_handle_t native_handle() const { return bus_handle_; }

protected:
Config config_;
i2c_master_bus_handle_t bus_handle_ = nullptr;
Expand Down
2 changes: 1 addition & 1 deletion components/m5stack-tab5/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
idf_component_register(
INCLUDE_DIRS "include"
SRC_DIRS "src"
REQUIRES driver esp_driver_i2s esp_driver_ppa esp_driver_sdmmc esp_driver_spi esp_lcd fatfs base_component bmi270 codec display display_drivers gt911 i2c ina226 input_drivers interrupt pi4ioe5v rx8130ce st7123touch task touch
REQUIRES driver esp_driver_i2s esp_driver_ppa esp_driver_sdmmc esp_driver_spi esp_lcd esp_video esp_cam_sensor fatfs base_component bmi270 codec display display_drivers gt911 i2c ina226 input_drivers interrupt pi4ioe5v rx8130ce st7123touch task touch
REQUIRED_IDF_TARGETS "esp32p4"
)
9 changes: 6 additions & 3 deletions components/m5stack-tab5/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,12 @@ The `espp::M5StackTab5` component provides a singleton hardware abstraction for
- Hi-Fi recording and playback capabilities

### Camera
- SC2356 2MP camera (1600 × 1200) via MIPI-CSI
- HD video recording and image processing
- Support for edge-AI applications
- MIPI-CSI camera (SC202CS) brought up via Espressif's `esp_video` (V4L2)
pipeline (CSI receiver + ISP), with the ISP converting the sensor's RAW8
output to RGB565
- `initialize_camera(callback)` streams frames to the callback from a capture
task; the sensor's SCCB shares the internal I2C bus
- See the example's Camera tab for a live-feed display

### Sensors & IMU
- BMI270 6-axis sensor (accelerometer + gyroscope)
Expand Down
10 changes: 9 additions & 1 deletion components/m5stack-tab5/example/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,15 @@
# in this exact order for cmake to work correctly
cmake_minimum_required(VERSION 3.20)

set(ENV{IDF_COMPONENT_MANAGER} "0")
# The component manager is needed to pull the managed camera components
# (espressif/esp_video + espressif/esp_cam_sensor). They are declared as
# dependencies of the m5stack-tab5 BSP (components/m5stack-tab5/idf_component.yml)
# and fetched transitively, so they do not need to be listed here. The espp
# components are provided locally via EXTRA_COMPONENT_DIRS below.
#
# Enable the component manager explicitly so this example configures even if the
# build environment defaults it off (it is on by default in a normal ESP-IDF).
set(ENV{IDF_COMPONENT_MANAGER} "1")
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
Comment thread
finger563 marked this conversation as resolved.

# add the component directories that we want to use
Expand Down
25 changes: 25 additions & 0 deletions components/m5stack-tab5/example/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,31 @@ This example demonstrates the comprehensive functionality of the M5Stack Tab5 de
before playback, and the per-channel peak amplitude is logged so you can
tell a silent capture (peak near 0) from a playback issue.
- **BMI270 6-axis IMU**: Real-time motion sensing
- **MIPI-CSI Camera (SC202CS)**: The **Camera** tab shows the live camera feed.
The BSP brings up the camera via Espressif's `esp_video` (V4L2) pipeline
(CSI + ISP, RAW8 -> RGB565) and streams frames to a capture task; each frame is
downscaled and rotated to the current display orientation by the PPA, then the
example copies it into an `lv_canvas` on the Camera tab. The managed
`espressif/esp_video` + `espressif/esp_cam_sensor` components are declared as
dependencies of the `m5stack-tab5` BSP (see
`components/m5stack-tab5/idf_component.yml`) and fetched transitively, so this
example builds with the IDF component manager enabled. The ISP pipeline
controller (auto exposure / white balance) is enabled so the feed is correctly
white-balanced.
- **Known upstream quirks (both benign, image unaffected):**
- The stock SC202CS auto color-correction sometimes computes a CCM value
beyond the ESP32-P4 ISP's `+/-4.0` limit under certain lighting. The ISP
safely rejects it and keeps the previous CCM, but the pipeline logs the
rejection every frame - enough blocking UART logging to drop frames - so
the BSP's `initialize_camera()` silences those ISP/CCM log tags. See the
comment in `components/m5stack-tab5/src/camera.cpp`.
- You may also see occasional `ISP: gamma xcoord error` lines (a few at
startup, then rarely on big scene-brightness changes). The precompiled
auto-enhancement (AEN) hands the ISP a gamma table the hardware rejects;
the ISP keeps the previous gamma, so the picture is fine and no frames are
dropped. It is logged from an ISR (`ESP_EARLY_LOGE`), which ignores the
runtime log-level controls, so it cannot be silenced the way the CCM spam
is without also hiding real errors - it is left as-is.
- **Battery Management**: INA226 power monitoring with charging control

### Communication Interfaces
Expand Down
235 changes: 235 additions & 0 deletions components/m5stack-tab5/example/main/gui.cpp
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
#include <algorithm>
#include <cstring>
#include <utility>

#include <esp_heap_caps.h>

#include "gui.hpp"

void Gui::init_ui() {
Expand All @@ -9,12 +12,38 @@ void Gui::init_ui() {
init_draw_tab();
init_status_tab();
init_audio_tab();
init_camera_tab();
init_circle_layer();
ui_ready_ = true;
}
Comment thread
finger563 marked this conversation as resolved.

void Gui::deinit_ui() {
std::lock_guard<std::recursive_mutex> lock(mutex_);
// Mark the UI down first so a camera frame arriving mid-teardown (the camera
// task runs independently) is dropped rather than touching freed objects.
ui_ready_ = false;
lv_obj_clean(lv_screen_active());
// lv_obj_clean() deleted every object under the screen; null the pointers we
// hold so nothing dereferences a freed object. It does not free the canvas's
// external PSRAM buffer (a member we own), so release that too.
tabview_ = nullptr;
draw_tab_ = nullptr;
status_tab_ = nullptr;
audio_tab_ = nullptr;
camera_tab_ = nullptr;
camera_canvas_ = nullptr;
camera_settings_btn_ = nullptr;
camera_panel_ = nullptr;
camera_scale_dd_ = nullptr;
camera_mirror_sw_ = nullptr;
camera_flip_sw_ = nullptr;
camera_label_ = nullptr;
if (camera_buf_) {
heap_caps_free(camera_buf_);
camera_buf_ = nullptr;
}
camera_w_ = 0;
camera_h_ = 0;
}
Comment thread
finger563 marked this conversation as resolved.

void Gui::init_tabview() {
Expand All @@ -28,6 +57,7 @@ void Gui::init_tabview() {
draw_tab_ = lv_tabview_add_tab(tabview_, "Draw");
status_tab_ = lv_tabview_add_tab(tabview_, "Status");
audio_tab_ = lv_tabview_add_tab(tabview_, "Audio");
camera_tab_ = lv_tabview_add_tab(tabview_, "Camera");
// switching tabs is done with the tab buttons only: disable swipe
// scrolling of the content so drawing on the Draw tab cannot accidentally
// change pages
Expand Down Expand Up @@ -166,6 +196,211 @@ void Gui::update_audio_label() {
LV_SYMBOL_PLUS);
}

void Gui::init_camera_tab() {
// Center the camera feed; show a placeholder label until the first frame
// arrives. The canvas that displays the live feed is created lazily in
// set_camera_frame() once the true frame size is known. The settings controls
// float on top of the feed (see build_camera_controls).
lv_obj_set_flex_flow(camera_tab_, LV_FLEX_FLOW_COLUMN);
lv_obj_set_flex_align(camera_tab_, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER,
LV_FLEX_ALIGN_CENTER);
// No padding, so a full-size feed can sit flush in the top-left corner.
lv_obj_set_style_pad_all(camera_tab_, 0, 0);
camera_label_ = lv_label_create(camera_tab_);
lv_label_set_text(camera_label_, "Waiting for camera...");
build_camera_controls();
}

void Gui::build_camera_controls() {
// A gear button in the top-right corner, floating (not part of the tab's flex
// layout) so it overlays the feed and is always visible. Tapping it toggles
// the settings panel.
camera_settings_btn_ = lv_btn_create(camera_tab_);
lv_obj_add_flag(camera_settings_btn_, LV_OBJ_FLAG_FLOATING);
lv_obj_set_size(camera_settings_btn_, 48, 48);
lv_obj_align(camera_settings_btn_, LV_ALIGN_TOP_RIGHT, -8, 8);
lv_obj_t *gear = lv_label_create(camera_settings_btn_);
lv_label_set_text(gear, LV_SYMBOL_SETTINGS);
lv_obj_center(gear);
lv_obj_add_event_cb(camera_settings_btn_, camera_settings_toggle_cb, LV_EVENT_CLICKED, this);

// The panel: floating, semi-transparent, sized to its content so it stays
// compact. Starts collapsed (hidden).
camera_panel_ = lv_obj_create(camera_tab_);
lv_obj_add_flag(camera_panel_, LV_OBJ_FLAG_FLOATING);
lv_obj_set_size(camera_panel_, 240, LV_SIZE_CONTENT);
lv_obj_align(camera_panel_, LV_ALIGN_TOP_RIGHT, -8, 64);
lv_obj_set_flex_flow(camera_panel_, LV_FLEX_FLOW_COLUMN);
lv_obj_set_style_pad_row(camera_panel_, 8, 0);
lv_obj_set_style_bg_opa(camera_panel_, LV_OPA_80, 0);
lv_obj_add_flag(camera_panel_, LV_OBJ_FLAG_HIDDEN);

lv_obj_t *title = lv_label_create(camera_panel_);
lv_label_set_text(title, "Camera settings");

// Image size dropdown.
lv_obj_t *size_label = lv_label_create(camera_panel_);
lv_label_set_text(size_label, "Image size");
camera_scale_dd_ = lv_dropdown_create(camera_panel_);
lv_dropdown_set_options(camera_scale_dd_, "Full\nHalf\nQuarter");
lv_dropdown_set_selected(camera_scale_dd_, 1); // Half (the BSP default)
lv_obj_set_width(camera_scale_dd_, lv_pct(100));
lv_obj_add_event_cb(camera_scale_dd_, camera_control_event_cb, LV_EVENT_VALUE_CHANGED, this);

// A labeled switch row helper.
auto add_switch = [&](const char *text, bool on) {
lv_obj_t *row = lv_obj_create(camera_panel_);
lv_obj_remove_style_all(row);
lv_obj_set_size(row, lv_pct(100), LV_SIZE_CONTENT);
lv_obj_set_flex_flow(row, LV_FLEX_FLOW_ROW);
lv_obj_set_flex_align(row, LV_FLEX_ALIGN_SPACE_BETWEEN, LV_FLEX_ALIGN_CENTER,
LV_FLEX_ALIGN_CENTER);
lv_obj_t *l = lv_label_create(row);
lv_label_set_text(l, text);
lv_obj_t *sw = lv_switch_create(row);
if (on) {
lv_obj_add_state(sw, LV_STATE_CHECKED);
}
lv_obj_add_event_cb(sw, camera_control_event_cb, LV_EVENT_VALUE_CHANGED, this);
return sw;
};
camera_mirror_sw_ = add_switch("Mirror", false);
camera_flip_sw_ = add_switch("Flip", false);
}

void Gui::camera_settings_toggle_cb(lv_event_t *e) {
auto *gui = static_cast<Gui *>(lv_event_get_user_data(e));
if (!gui || !gui->camera_panel_) {
return;
}
if (lv_obj_has_flag(gui->camera_panel_, LV_OBJ_FLAG_HIDDEN)) {
lv_obj_clear_flag(gui->camera_panel_, LV_OBJ_FLAG_HIDDEN);
lv_obj_move_foreground(gui->camera_panel_);
} else {
lv_obj_add_flag(gui->camera_panel_, LV_OBJ_FLAG_HIDDEN);
}
}

void Gui::camera_control_event_cb(lv_event_t *e) {
auto *gui = static_cast<Gui *>(lv_event_get_user_data(e));
if (gui) {
gui->sync_camera_controls();
}
}

void Gui::sync_camera_controls() {
// Read every widget into camera_controls_ and push it to the BSP. (Called
// from an LVGL event, i.e. already under the GUI mutex via lv_task_handler.)
using Scale = espp::M5StackTab5::CameraScale;
uint32_t scale_idx = lv_dropdown_get_selected(camera_scale_dd_);
camera_controls_.scale = (scale_idx == 0) ? Scale::FULL
: (scale_idx == 2) ? Scale::QUARTER
: Scale::HALF;
camera_controls_.hmirror = lv_obj_has_state(camera_mirror_sw_, LV_STATE_CHECKED);
camera_controls_.vflip = lv_obj_has_state(camera_flip_sw_, LV_STATE_CHECKED);
espp::M5StackTab5::get().set_camera_controls(camera_controls_);
}

void Gui::raise_camera_controls() {
// Keep the overlay above the camera canvas, which is (re)created lazily and
// would otherwise cover the controls.
if (camera_settings_btn_) {
lv_obj_move_foreground(camera_settings_btn_);
}
if (camera_panel_) {
lv_obj_move_foreground(camera_panel_);
}
}

void Gui::camera_canvas_drag_cb(lv_event_t *e) {
auto *canvas = static_cast<lv_obj_t *>(lv_event_get_target(e));
lv_indev_t *indev = lv_indev_active();
if (!canvas || !indev) {
return;
}
// Move the feed by the drag delta since the last event.
lv_point_t vect;
lv_indev_get_vect(indev, &vect);
lv_obj_t *parent = lv_obj_get_parent(canvas);
int32_t x = lv_obj_get_x(canvas) + vect.x;
int32_t y = lv_obj_get_y(canvas) + vect.y;
// Keep the feed within the tab; if it is larger than the tab, the range goes
// negative so its far edges can still be panned into view.
int32_t range_x = lv_obj_get_width(parent) - lv_obj_get_width(canvas);
int32_t range_y = lv_obj_get_height(parent) - lv_obj_get_height(canvas);
int32_t lo_x = LV_MIN(0, range_x), hi_x = LV_MAX(0, range_x);
int32_t lo_y = LV_MIN(0, range_y), hi_y = LV_MAX(0, range_y);
x = x < lo_x ? lo_x : (x > hi_x ? hi_x : x);
y = y < lo_y ? lo_y : (y > hi_y ? hi_y : y);
lv_obj_set_pos(canvas, x, y);
}

void Gui::set_camera_frame(const uint8_t *rgb565, int w, int h) {
if (!rgb565 || w <= 0 || h <= 0) {
return;
}
std::lock_guard<std::recursive_mutex> lock(mutex_);
// The camera task may still deliver a frame after the UI is torn down; drop it
// rather than create objects under a freed camera_tab_.
if (!ui_ready_ || !camera_tab_) {
return;
}
// (Re)allocate the canvas buffer and (re)create the canvas on the first frame
// or whenever the frame size changes. The buffer must outlive the canvas, so
// it is a member kept in PSRAM.
if (camera_buf_ == nullptr || w != camera_w_ || h != camera_h_) {
// Allocate the new buffer BEFORE freeing the old one / deleting the canvas,
// so that on OOM we keep showing the previous frame instead of blanking the
// tab.
const size_t bytes = static_cast<size_t>(w) * static_cast<size_t>(h) * 2;
auto *new_buf =
static_cast<uint8_t *>(heap_caps_malloc(bytes, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT));
if (!new_buf) {
return; // out of memory; keep the current canvas / preview
}
if (camera_canvas_) {
lv_obj_del(camera_canvas_);
camera_canvas_ = nullptr;
}
if (camera_buf_) {
heap_caps_free(camera_buf_);
}
camera_buf_ = new_buf;
camera_w_ = w;
camera_h_ = h;
camera_canvas_ = lv_canvas_create(camera_tab_);
lv_canvas_set_buffer(camera_canvas_, camera_buf_, w, h, LV_COLOR_FORMAT_RGB565);
// Frame the feed (a border + rounded corners so it reads as a distinct
// element when it is smaller than the tab), and make it draggable within the
// tab. LV_OBJ_FLAG_FLOATING takes it out of the tab's flex layout so it can
// be freely positioned; the border brightens while it is pressed to hint
// that it can be moved.
lv_obj_add_flag(camera_canvas_, LV_OBJ_FLAG_FLOATING);
lv_obj_add_flag(camera_canvas_, LV_OBJ_FLAG_CLICKABLE);
lv_obj_set_style_border_width(camera_canvas_, 2, 0);
lv_obj_set_style_border_color(camera_canvas_, lv_palette_main(LV_PALETTE_GREY), 0);
lv_obj_set_style_border_color(camera_canvas_, lv_palette_main(LV_PALETTE_BLUE),
LV_STATE_PRESSED);
lv_obj_set_style_radius(camera_canvas_, 6, 0);
lv_obj_add_event_cb(camera_canvas_, camera_canvas_drag_cb, LV_EVENT_PRESSING, this);
// Center the feed when it fits within the tab; otherwise pin it to the
// top-left so it fills from the corner (drag to pan the overflow).
lv_obj_update_layout(camera_tab_);
const int32_t tab_w = lv_obj_get_width(camera_tab_);
const int32_t tab_h = lv_obj_get_height(camera_tab_);
const bool fits = (w <= tab_w) && (h <= tab_h);
lv_obj_set_pos(camera_canvas_, fits ? (tab_w - w) / 2 : 0, fits ? (tab_h - h) / 2 : 0);
if (camera_label_) {
lv_obj_add_flag(camera_label_, LV_OBJ_FLAG_HIDDEN);
}
// the canvas was just created on top of the settings overlay; restore the
// overlay to the foreground so the gear button / panel stay tappable.
raise_camera_controls();
}
std::memcpy(camera_buf_, rgb565, static_cast<size_t>(w) * static_cast<size_t>(h) * 2);
lv_obj_invalidate(camera_canvas_);
}

void Gui::init_circle_layer() {
// a transparent, click-through overlay above the tabview which shows the
// touch trail (only populated while the Draw tab is active)
Expand Down
Loading
Loading