Merge develop into main (#368)

New boards:
- LilyGO T-Dongle S3
- M5Stack StickC Plus
- M5Stack StickC Plus2

New drivers:
- AXP192: power control via I2C
- ButtonControl: GPIO button input as LVGL device

Other changes:
- Updated implementation of AXP192 driver for Core2 board
- Fix launcher UX for vertical layout
- Fix error when properties file had an empty line
- Add `__floatsidf` to `tt_init.cpp`
This commit is contained in:
Ken Van Hoeylandt
2025-10-14 20:39:23 +02:00
committed by GitHub
parent 3a59540365
commit d8346998ce
68 changed files with 1857 additions and 142 deletions
@@ -0,0 +1,91 @@
#ifdef ESP_PLATFORM
#pragma once
#include "SdCardDevice.h"
#include <Tactility/hal/spi/Spi.h>
#include <sd_protocol_types.h>
#include <utility>
#include <vector>
#include <Tactility/hal/Device.h>
#include <hal/spi_types.h>
#include <soc/gpio_num.h>
namespace tt::hal::sdcard {
/**
* SD card interface for the SDMMC interface.
*/
class SdmmcDevice final : public SdCardDevice {
std::shared_ptr<Mutex> mutex = std::make_shared<Mutex>(Mutex::Type::Recursive);
public:
struct Config {
Config(
gpio_num_t pinClock,
gpio_num_t pinCmd,
gpio_num_t pinD0,
gpio_num_t pinD1,
gpio_num_t pinD2,
gpio_num_t pinD3,
MountBehaviour mountBehaviourAtBoot
) :
pinClock(pinClock),
pinCmd(pinCmd),
pinD0(pinD0),
pinD1(pinD1),
pinD2(pinD2),
pinD3(pinD3),
mountBehaviourAtBoot(mountBehaviourAtBoot)
{}
int spiFrequencyKhz;
gpio_num_t pinClock;
gpio_num_t pinCmd;
gpio_num_t pinD0;
gpio_num_t pinD1;
gpio_num_t pinD2;
gpio_num_t pinD3;
MountBehaviour mountBehaviourAtBoot;
bool formatOnMountFailed = false;
uint16_t maxOpenFiles = 4;
uint16_t allocUnitSize = 16 * 1024;
bool statusCheckEnabled = false;
};
private:
std::string mountPath;
sdmmc_card_t* card = nullptr;
std::shared_ptr<Config> config;
bool applyGpioWorkAround();
bool mountInternal(const std::string& mountPath);
public:
explicit SdmmcDevice(std::unique_ptr<Config> config) : SdCardDevice(config->mountBehaviourAtBoot),
config(std::move(config))
{}
std::string getName() const override { return "SDMMC"; }
std::string getDescription() const override { return "SD card via SDMMC interface"; }
bool mount(const std::string& mountPath) override;
bool unmount() override;
std::string getMountPath() const override { return mountPath; }
std::shared_ptr<Lock> getLock() const override { return mutex; }
State getState(TickType_t timeout) const override;
sdmmc_card_t* _Nullable getCard() { return card; }
};
}
#endif
@@ -4,6 +4,6 @@
namespace tt {
void initFromBootApp();
void registerApps();
}
+12 -2
View File
@@ -201,6 +201,13 @@ static void registerInstalledAppsFromSdCards() {
}
}
static void registerInstalledAppsFromData() {
auto app_path = "/data/app";
if (file::isDirectory(app_path)) {
registerInstalledApps(app_path);
}
}
static void registerAndStartSecondaryServices() {
TT_LOG_I(TAG, "Registering and starting system services");
addService(service::loader::manifest);
@@ -214,7 +221,9 @@ static void registerAndStartSecondaryServices() {
static void registerAndStartPrimaryServices() {
TT_LOG_I(TAG, "Registering and starting system services");
addService(service::gps::manifest);
addService(service::sdcard::manifest);
if (hal::hasDevice(hal::Device::Type::SdCard)) {
addService(service::sdcard::manifest);
}
addService(service::wifi::manifest);
#ifdef ESP_PLATFORM
addService(service::development::manifest);
@@ -222,13 +231,14 @@ static void registerAndStartPrimaryServices() {
#endif
}
void initFromBootApp() {
void registerApps() {
registerInternalApps();
auto data_apps_path = std::format("{}/apps", file::MOUNT_POINT_DATA);
if (file::isDirectory(data_apps_path)) {
registerInstalledApps(data_apps_path);
}
registerInstalledAppsFromSdCards();
registerInstalledAppsFromData();
}
void run(const Configuration& config) {
+1 -1
View File
@@ -76,7 +76,7 @@ private:
assert(elfFileData == nullptr);
size_t size = 0;
file::withLock<void>(elf_path, [this, &elf_path, &size]{
file::getLock(elf_path)->withLock([this, &elf_path, &size]{
elfFileData = file::readBinary(elf_path, size);
});
+1 -1
View File
@@ -108,7 +108,7 @@ class BootApp : public App {
if (!setupUsbBootMode()) {
TT_LOG_I(TAG, "initFromBootApp");
initFromBootApp();
registerApps();
waitForMinimalSplashDuration(start_time);
stop(manifest.appId);
startNextApp();
+18 -7
View File
@@ -24,12 +24,17 @@ static int getButtonSize(hal::UiScale scale) {
class LauncherApp final : public App {
static lv_obj_t* createAppButton(lv_obj_t* parent, hal::UiScale uiScale, const char* imageFile, const char* appId, int32_t horizontalMargin) {
static lv_obj_t* createAppButton(lv_obj_t* parent, hal::UiScale uiScale, const char* imageFile, const char* appId, int32_t itemMargin, bool isLandscape) {
auto button_size = getButtonSize(uiScale);
auto* apps_button = lv_button_create(parent);
lv_obj_set_style_pad_all(apps_button, 0, LV_STATE_DEFAULT);
lv_obj_set_style_margin_hor(apps_button, horizontalMargin, LV_STATE_DEFAULT);
if (isLandscape) {
lv_obj_set_style_margin_hor(apps_button, itemMargin, LV_STATE_DEFAULT);
} else {
lv_obj_set_style_margin_ver(apps_button, itemMargin, LV_STATE_DEFAULT);
}
lv_obj_set_style_shadow_width(apps_button, 0, LV_STATE_DEFAULT);
lv_obj_set_style_bg_opa(apps_button, 0, LV_STATE_DEFAULT);
@@ -110,17 +115,23 @@ public:
lv_obj_set_flex_flow(buttons_wrapper, LV_FLEX_FLOW_COLUMN);
}
const int32_t available_width = lv_display_get_horizontal_resolution(display) - (3 * button_size);
const int32_t margin = is_landscape_display ? std::min<int32_t>(available_width / 16, button_size) : 0;
int32_t margin;
if (is_landscape_display) {
const int32_t available_width = std::max<int32_t>(0, lv_display_get_horizontal_resolution(display) - (3 * button_size));
margin = std::min<int32_t>(available_width / 16, button_size);
} else {
const int32_t available_height = std::max<int32_t>(0, lv_display_get_vertical_resolution(display) - (3 * button_size));
margin = std::min<int32_t>(available_height / 16, button_size);
}
const auto paths = app.getPaths();
const auto apps_icon_path = lvgl::PATH_PREFIX + paths->getAssetsPath("icon_apps.png");
const auto files_icon_path = lvgl::PATH_PREFIX + paths->getAssetsPath("icon_files.png");
const auto settings_icon_path = lvgl::PATH_PREFIX + paths->getAssetsPath("icon_settings.png");
createAppButton(buttons_wrapper, ui_scale, apps_icon_path.c_str(), "AppList", margin);
createAppButton(buttons_wrapper, ui_scale, files_icon_path.c_str(), "Files", margin);
createAppButton(buttons_wrapper, ui_scale, settings_icon_path.c_str(), "Settings", margin);
createAppButton(buttons_wrapper, ui_scale, apps_icon_path.c_str(), "AppList", margin, is_landscape_display);
createAppButton(buttons_wrapper, ui_scale, files_icon_path.c_str(), "Files", margin, is_landscape_display);
createAppButton(buttons_wrapper, ui_scale, settings_icon_path.c_str(), "Settings", margin, is_landscape_display);
if (shouldShowPowerButton()) {
auto* power_button = lv_btn_create(parent);
+5 -5
View File
@@ -83,7 +83,7 @@ class NotesApp final : public App {
void openFile(const std::string& path) {
// We might be reading from the SD card, which could share a SPI bus with other devices (display)
file::withLock<void>(path, [this, path] {
file::getLock(path)->withLock([this, path] {
auto data = file::readString(path);
if (data != nullptr) {
auto lock = lvgl::getSyncLock()->asScopedLock();
@@ -98,15 +98,15 @@ class NotesApp final : public App {
bool saveFile(const std::string& path) {
// We might be writing to SD card, which could share a SPI bus with other devices (display)
return file::withLock<bool>(path, [this, path] {
bool result = false;
file::getLock(path)->withLock([&result, this, path] {
if (file::writeString(path, saveBuffer.c_str())) {
TT_LOG_I(TAG, "Saved to %s", path.c_str());
filePath = path;
return true;
} else {
return false;
result = true;
}
});
return result;
}
#pragma endregion Open_Events_Functions
+6 -4
View File
@@ -26,7 +26,7 @@ bool loadPropertiesFile(const std::string& filePath, std::function<void(const st
line_count++;
std::string key, value;
auto trimmed_line = string::trim(line, " \t");
if (!trimmed_line.starts_with("#")) {
if (!trimmed_line.starts_with("#") && !trimmed_line.empty()) {
if (trimmed_line.starts_with("[")) {
key_prefix = trimmed_line;
} else {
@@ -49,20 +49,22 @@ bool loadPropertiesFile(const std::string& filePath, std::map<std::string, std::
}
bool savePropertiesFile(const std::string& filePath, const std::map<std::string, std::string>& properties) {
return file::withLock<bool>(filePath, [filePath, &properties] {
bool result = false;
getLock(filePath)->withLock([&result, filePath, &properties] {
TT_LOG_I(TAG, "Saving properties file %s", filePath.c_str());
FILE* file = fopen(filePath.c_str(), "w");
if (file == nullptr) {
TT_LOG_E(TAG, "Failed to open %s", filePath.c_str());
return false;
return;
}
for (const auto& [key, value]: properties) { fprintf(file, "%s=%s\n", key.c_str(), value.c_str()); }
fclose(file);
return true;
result = true;
});
return result;
}
}
+113
View File
@@ -0,0 +1,113 @@
#ifdef ESP_PLATFORM
#include <Tactility/hal/sdcard/SdmmcDevice.h>
#include <Tactility/Log.h>
#include <esp_vfs_fat.h>
#include <sdmmc_cmd.h>
#include <driver/sdmmc_host.h>
namespace tt::hal::sdcard {
constexpr auto* TAG = "SdmmcDevice";
bool SdmmcDevice::mountInternal(const std::string& newMountPath) {
TT_LOG_I(TAG, "Mounting %s", newMountPath.c_str());
esp_vfs_fat_sdmmc_mount_config_t mount_config = {
.format_if_mount_failed = config->formatOnMountFailed,
.max_files = config->maxOpenFiles,
.allocation_unit_size = config->allocUnitSize,
.disk_status_check_enable = config->statusCheckEnabled,
.use_one_fat = false
};
sdmmc_host_t host = SDMMC_HOST_DEFAULT();
sdmmc_slot_config_t slot_config = {
.clk = config->pinClock,
.cmd = config->pinCmd,
.d0 = config->pinD0,
.d1 = config->pinD1,
.d2 = config->pinD2,
.d3 = config->pinD3,
.d4 = static_cast<gpio_num_t>(0),
.d5 = static_cast<gpio_num_t>(0),
.d6 = static_cast<gpio_num_t>(0),
.d7 = static_cast<gpio_num_t>(0),
.cd = GPIO_NUM_NC,
.wp = GPIO_NUM_NC,
.width = 4,
.flags = 0
};
esp_err_t result = esp_vfs_fat_sdmmc_mount(newMountPath.c_str(), &host, &slot_config, &mount_config, &card);
if (result != ESP_OK || card == nullptr) {
if (result == ESP_FAIL) {
TT_LOG_E(TAG, "Mounting failed. Ensure the card is formatted with FAT.");
} else {
TT_LOG_E(TAG, "Mounting failed (%s)", esp_err_to_name(result));
}
return false;
}
mountPath = newMountPath;
return true;
}
bool SdmmcDevice::mount(const std::string& newMountPath) {
if (mountInternal(newMountPath)) {
TT_LOG_I(TAG, "Mounted at %s", newMountPath.c_str());
sdmmc_card_print_info(stdout, card);
return true;
} else {
TT_LOG_E(TAG, "Mount failed for %s", newMountPath.c_str());
return false;
}
}
bool SdmmcDevice::unmount() {
if (card == nullptr) {
TT_LOG_E(TAG, "Can't unmount: not mounted");
return false;
}
if (esp_vfs_fat_sdcard_unmount(mountPath.c_str(), card) != ESP_OK) {
TT_LOG_E(TAG, "Unmount failed for %s", mountPath.c_str());
return false;
}
TT_LOG_I(TAG, "Unmounted %s", mountPath.c_str());
mountPath = "";
card = nullptr;
return true;
}
SdmmcDevice::State SdmmcDevice::getState(TickType_t timeout) const {
if (card == nullptr) {
return State::Unmounted;
}
/**
* The SD card and the screen are on the same SPI bus.
* Writing and reading to the bus from 2 devices at the same time causes crashes.
* This work-around ensures that this check is only happening when LVGL isn't rendering.
*/
auto lock = getLock()->asScopedLock();
bool locked = lock.lock(timeout);
if (!locked) {
return State::Timeout;
}
if (sdmmc_get_status(card) != ESP_OK) {
return State::Error;
}
return State::Mounted;
}
}
#endif
+3 -2
View File
@@ -7,8 +7,9 @@ namespace tt::lvgl {
constexpr auto* TAG = "LabelUtils";
bool label_set_text_file(lv_obj_t* label, const char* filepath) {
auto text = file::withLock<std::unique_ptr<uint8_t[]>>(std::string(filepath), [filepath] {
return file::readString(filepath);
std::unique_ptr<uint8_t[]> text;
file::getLock(filepath)->withLock([&text, filepath] {
text = file::readString(filepath);
});
if (text != nullptr) {