Merge develop into main branch (#137)

* SdCard HAL refactored (#135)

- Refactor SdCard HAL
- introduce Lockable

* Screenshot and FatFS improvements (#136)

- Fix screenshots on ESP32
- Improve Screenshot service
- Convert Screenshot app to class-based instead of structs
- Screenshot app now automatically updates when task is finished
- Enable FatFS long filename support

* Re-use common log messages (#138)

For consistency and binary size reduction

* Toolbar spinner should get margin to the right

* More TactilityC features (#139)

* Rewrote Loader

- Simplified Loader by removing custom threa
- Created DispatcherThread
- Move auto-starting apps to Boot app
- Fixed Dispatcher bug where it could get stuck not processing new
messages

* Hide AP settings if the AP is not saved

* Missing from previous commit

* Replace LV_EVENT_CLICKED with LV_EVENT_SHORT_CLICKED

* Refactored files app and created InputDialog (#140)

- Changed Files app so that it has a View and State
- Files app now allows for long-pressing on files to perform actions
- Files app now has rename and delete actions
- Created InputDialog app
- Improved AlertDialog app layout
This commit is contained in:
Ken Van Hoeylandt 2024-12-27 22:12:39 +00:00 committed by GitHub
parent 9033daa6dd
commit 50bd6e8bf6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
144 changed files with 3244 additions and 2038 deletions

View File

@ -2,7 +2,7 @@
// Apps // Apps
#include "Tactility.h" #include "Tactility.h"
#include "TactilityC/TactilityC.h" #include "tt_init.h"
namespace tt::service::wifi { namespace tt::service::wifi {
extern void wifi_task(void*); extern void wifi_task(void*);

View File

@ -1,5 +1,5 @@
idf_component_register( idf_component_register(
SRC_DIRS "Source" "Source/hal" SRC_DIRS "Source" "Source/hal"
INCLUDE_DIRS "Source" INCLUDE_DIRS "Source"
REQUIRES Tactility esp_lvgl_port esp_lcd esp_lcd_touch_gt911 driver vfs fatfs esp_adc REQUIRES Tactility esp_lvgl_port esp_lcd esp_lcd_touch_gt911 driver esp_adc
) )

View File

@ -19,25 +19,32 @@
#define TDECK_LCD_BACKLIGHT_LEDC_FREQUENCY (4000) #define TDECK_LCD_BACKLIGHT_LEDC_FREQUENCY (4000)
static bool init_spi() { static bool init_spi() {
TT_LOG_I(TAG, LOG_MESSAGE_SPI_INIT_START_FMT, TDECK_SPI_HOST);
spi_bus_config_t bus_config = { spi_bus_config_t bus_config = {
.mosi_io_num = TDECK_SPI_PIN_MOSI, .mosi_io_num = TDECK_SPI_PIN_MOSI,
.miso_io_num = TDECK_SPI_PIN_MISO, .miso_io_num = TDECK_SPI_PIN_MISO,
.sclk_io_num = TDECK_SPI_PIN_SCLK, .sclk_io_num = TDECK_SPI_PIN_SCLK,
.quadwp_io_num = -1, // Quad SPI LCD driver is not yet supported .quadwp_io_num = -1, // Quad SPI LCD driver is not yet supported
.quadhd_io_num = -1, // Quad SPI LCD driver is not yet supported .quadhd_io_num = -1, // Quad SPI LCD driver is not yet supported
.data4_io_num = 0,
.data5_io_num = 0,
.data6_io_num = 0,
.data7_io_num = 0,
.max_transfer_sz = TDECK_SPI_TRANSFER_SIZE_LIMIT, .max_transfer_sz = TDECK_SPI_TRANSFER_SIZE_LIMIT,
.flags = 0,
.isr_cpu_id = ESP_INTR_CPU_AFFINITY_AUTO,
.intr_flags = 0
}; };
return spi_bus_initialize(TDECK_SPI_HOST, &bus_config, SPI_DMA_CH_AUTO) == ESP_OK; if (spi_bus_initialize(TDECK_SPI_HOST, &bus_config, SPI_DMA_CH_AUTO) != ESP_OK) {
} TT_LOG_E(TAG, LOG_MESSAGE_SPI_INIT_FAILED_FMT, TDECK_SPI_HOST);
bool tdeck_init_hardware() {
TT_LOG_I(TAG, "Init SPI");
if (!init_spi()) {
TT_LOG_E(TAG, "Init SPI failed");
return false; return false;
} }
return true; return true;
} }
bool tdeck_init_hardware() {
return init_spi();
}

View File

@ -2,21 +2,19 @@
#include "hal/TdeckDisplay.h" #include "hal/TdeckDisplay.h"
#include "hal/TdeckKeyboard.h" #include "hal/TdeckKeyboard.h"
#include "hal/TdeckPower.h" #include "hal/TdeckPower.h"
#include "hal/sdcard/Sdcard.h" #include "hal/TdeckSdCard.h"
bool tdeck_init_power(); bool tdeck_init_power();
bool tdeck_init_hardware(); bool tdeck_init_hardware();
bool tdeck_init_lvgl(); bool tdeck_init_lvgl();
extern const tt::hal::sdcard::SdCard tdeck_sdcard;
extern const tt::hal::Configuration lilygo_tdeck = { extern const tt::hal::Configuration lilygo_tdeck = {
.initBoot = tdeck_init_power, .initBoot = tdeck_init_power,
.initHardware = tdeck_init_hardware, .initHardware = tdeck_init_hardware,
.initLvgl = tdeck_init_lvgl, .initLvgl = tdeck_init_lvgl,
.createDisplay = createDisplay, .createDisplay = createDisplay,
.createKeyboard = createKeyboard, .createKeyboard = createKeyboard,
.sdcard = &tdeck_sdcard, .sdcard = createTdeckSdCard(),
.power = tdeck_get_power, .power = tdeck_get_power,
.i2c = { .i2c = {
tt::hal::i2c::Configuration { tt::hal::i2c::Configuration {

View File

@ -27,9 +27,9 @@ static bool tdeck_power_on() {
} }
bool tdeck_init_power() { bool tdeck_init_power() {
ESP_LOGI(TAG, "Power on"); ESP_LOGI(TAG, LOG_MESSAGE_POWER_ON_START);
if (!tdeck_power_on()) { if (!tdeck_power_on()) {
TT_LOG_E(TAG, "Power on failed"); TT_LOG_E(TAG, LOG_MESSAGE_POWER_ON_FAILED);
return false; return false;
} }

View File

@ -1,166 +0,0 @@
#include "hal/sdcard/Sdcard.h"
#include "Check.h"
#include "Log.h"
#include "esp_vfs_fat.h"
#include "sdmmc_cmd.h"
#include "lvgl/LvglSync.h"
#define TAG "tdeck_sdcard"
#define TDECK_SDCARD_SPI_HOST SPI2_HOST
#define TDECK_SDCARD_PIN_CS GPIO_NUM_39
#define TDECK_SDCARD_SPI_FREQUENCY 800000U
#define TDECK_SDCARD_FORMAT_ON_MOUNT_FAILED false
#define TDECK_SDCARD_MAX_OPEN_FILES 4
#define TDECK_SDCARD_ALLOC_UNIT_SIZE (16 * 1024)
#define TDECK_SDCARD_STATUS_CHECK_ENABLED false
// Other
#define TDECK_LCD_PIN_CS GPIO_NUM_12
#define TDECK_RADIO_PIN_CS GPIO_NUM_9
typedef struct {
const char* mount_point;
sdmmc_card_t* card;
} MountData;
/**
* Before we can initialize the sdcard's SPI communications, we have to set all
* other SPI pins on the board high.
* See https://github.com/espressif/esp-idf/issues/1597
* See https://github.com/Xinyuan-LilyGO/T-Deck/blob/master/examples/UnitTest/UnitTest.ino
* @return success result
*/
static bool sdcard_init() {
TT_LOG_D(TAG, "init");
gpio_config_t config = {
.pin_bit_mask = BIT64(TDECK_SDCARD_PIN_CS) | BIT64(TDECK_RADIO_PIN_CS) | BIT64(TDECK_LCD_PIN_CS),
.mode = GPIO_MODE_OUTPUT,
.pull_up_en = GPIO_PULLUP_DISABLE,
.pull_down_en = GPIO_PULLDOWN_DISABLE,
.intr_type = GPIO_INTR_DISABLE,
};
if (gpio_config(&config) != ESP_OK) {
TT_LOG_E(TAG, "GPIO init failed");
return false;
}
if (gpio_set_level(TDECK_SDCARD_PIN_CS, 1) != ESP_OK) {
TT_LOG_E(TAG, "Failed to set board CS pin high");
return false;
}
if (gpio_set_level(TDECK_RADIO_PIN_CS, 1) != ESP_OK) {
TT_LOG_E(TAG, "Failed to set radio CS pin high");
return false;
}
if (gpio_set_level(TDECK_LCD_PIN_CS, 1) != ESP_OK) {
TT_LOG_E(TAG, "Failed to set TFT CS pin high");
return false;
}
return true;
}
static void* _Nullable sdcard_mount(const char* mount_point) {
TT_LOG_I(TAG, "Mounting %s", mount_point);
esp_vfs_fat_sdmmc_mount_config_t mount_config = {
.format_if_mount_failed = TDECK_SDCARD_FORMAT_ON_MOUNT_FAILED,
.max_files = TDECK_SDCARD_MAX_OPEN_FILES,
.allocation_unit_size = TDECK_SDCARD_ALLOC_UNIT_SIZE,
.disk_status_check_enable = TDECK_SDCARD_STATUS_CHECK_ENABLED
};
// Init without card detect (CD) and write protect (WD)
sdspi_device_config_t slot_config = SDSPI_DEVICE_CONFIG_DEFAULT();
slot_config.gpio_cs = TDECK_SDCARD_PIN_CS;
slot_config.host_id = TDECK_SDCARD_SPI_HOST;
sdmmc_host_t host = SDSPI_HOST_DEFAULT();
// The following value is from T-Deck repo's UnitTest.ino project:
// https://github.com/Xinyuan-LilyGO/T-Deck/blob/master/examples/UnitTest/UnitTest.ino
// Observation: Using this automatically sets the bus to 20MHz
host.max_freq_khz = TDECK_SDCARD_SPI_FREQUENCY;
sdmmc_card_t* card;
esp_err_t ret = esp_vfs_fat_sdspi_mount(mount_point, &host, &slot_config, &mount_config, &card);
if (ret != ESP_OK) {
if (ret == 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(ret));
}
return nullptr;
}
auto* data = static_cast<MountData*>(malloc(sizeof(MountData)));
*data = (MountData) {
.mount_point = mount_point,
.card = card,
};
return data;
}
static void* sdcard_init_and_mount(const char* mount_point) {
if (!sdcard_init()) {
TT_LOG_E(TAG, "Failed to set SPI CS pins high. This is a pre-requisite for mounting.");
return nullptr;
}
auto* data = static_cast<MountData*>(sdcard_mount(mount_point));
if (data == nullptr) {
TT_LOG_E(TAG, "Mount failed for %s", mount_point);
return nullptr;
}
sdmmc_card_print_info(stdout, data->card);
return data;
}
static void sdcard_unmount(void* context) {
auto* data = static_cast<MountData*>(context);
TT_LOG_I(TAG, "Unmounting %s", data->mount_point);
tt_assert(data != nullptr);
if (esp_vfs_fat_sdcard_unmount(data->mount_point, data->card) != ESP_OK) {
TT_LOG_E(TAG, "Unmount failed for %s", data->mount_point);
}
free(data);
}
// TODO: Refactor to "bool getStatus(Status* status)" method so that it can fail when the lvgl lock fails
static bool sdcard_is_mounted(void* context) {
auto* data = static_cast<MountData*>(context);
/**
* 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.
*/
bool locked = tt::lvgl::lock(50); // TODO: Refactor to a more reliable locking mechanism
if (!locked) {
TT_LOG_W(TAG, "Failed to get LVGL lock");
}
bool result = (data != nullptr) && (sdmmc_get_status(data->card) == ESP_OK);
if (locked) {
tt::lvgl::unlock();
}
return result;
}
extern const tt::hal::sdcard::SdCard tdeck_sdcard = {
.mount = &sdcard_init_and_mount,
.unmount = &sdcard_unmount,
.is_mounted = &sdcard_is_mounted,
.mount_behaviour = tt::hal::sdcard::MountBehaviourAtBoot
};

View File

@ -0,0 +1,34 @@
#include "TdeckSdCard.h"
#include "lvgl/LvglSync.h"
#include "hal/SpiSdCard.h"
#include <esp_vfs_fat.h>
#include <sdmmc_cmd.h>
#define TDECK_SDCARD_SPI_FREQUENCY 800000U
#define TDECK_SDCARD_PIN_CS GPIO_NUM_39
#define TDECK_LCD_PIN_CS GPIO_NUM_12
#define TDECK_RADIO_PIN_CS GPIO_NUM_9
std::shared_ptr<SdCard> createTdeckSdCard() {
auto* configuration = new tt::hal::SpiSdCard::Config(
TDECK_SDCARD_SPI_FREQUENCY,
TDECK_SDCARD_PIN_CS,
GPIO_NUM_NC,
GPIO_NUM_NC,
GPIO_NUM_NC,
SdCard::MountBehaviourAtBoot,
tt::lvgl::getLvglSyncLockable(),
{
TDECK_RADIO_PIN_CS,
TDECK_LCD_PIN_CS
}
);
auto* sdcard = (SdCard*) new SpiSdCard(
std::unique_ptr<SpiSdCard::Config>(configuration)
);
return std::shared_ptr<SdCard>(sdcard);
}

View File

@ -0,0 +1,7 @@
#pragma once
#include "hal/SdCard.h"
using namespace tt::hal;
std::shared_ptr<SdCard> createTdeckSdCard();

View File

@ -1,12 +1,11 @@
#include "M5stackCore2.h" #include "M5stackCore2.h"
#include "M5stackShared.h" #include "M5stackShared.h"
#include "hal/M5stackPower.h"
extern const tt::hal::Configuration m5stack_core2 = { extern const tt::hal::Configuration m5stack_core2 = {
.initBoot = m5stack_bootstrap, .initBoot = m5stack_bootstrap,
.initLvgl = m5stack_lvgl_init, .initLvgl = m5stack_lvgl_init,
.createDisplay = createDisplay, .createDisplay = createDisplay,
.sdcard = &m5stack_sdcard, .sdcard = createM5SdCard(),
.power = m5stack_get_power, .power = m5stack_get_power,
.i2c = { .i2c = {
tt::hal::i2c::Configuration { tt::hal::i2c::Configuration {

View File

@ -1,12 +1,11 @@
#include "M5stackCoreS3.h" #include "M5stackCoreS3.h"
#include "M5stackShared.h" #include "M5stackShared.h"
#include "hal/M5stackPower.h"
const tt::hal::Configuration m5stack_cores3 = { const tt::hal::Configuration m5stack_cores3 = {
.initBoot = m5stack_bootstrap, .initBoot = m5stack_bootstrap,
.initLvgl = m5stack_lvgl_init, .initLvgl = m5stack_lvgl_init,
.createDisplay = createDisplay, .createDisplay = createDisplay,
.sdcard = &m5stack_sdcard, .sdcard = createM5SdCard(),
.power = m5stack_get_power, .power = m5stack_get_power,
.i2c = { .i2c = {
tt::hal::i2c::Configuration { tt::hal::i2c::Configuration {

View File

@ -3,9 +3,8 @@
#include "hal/Power.h" #include "hal/Power.h"
#include "hal/M5stackTouch.h" #include "hal/M5stackTouch.h"
#include "hal/M5stackDisplay.h" #include "hal/M5stackDisplay.h"
#include "hal/sdcard/Sdcard.h" #include "hal/M5stackPower.h"
#include "hal/M5stackSdCard.h"
extern bool m5stack_bootstrap(); extern bool m5stack_bootstrap();
extern bool m5stack_lvgl_init(); extern bool m5stack_lvgl_init();
extern const tt::hal::sdcard::SdCard m5stack_sdcard;

View File

@ -1,137 +0,0 @@
#include "Check.h"
#include "Log.h"
#include "hal/sdcard/Sdcard.h"
#include "esp_vfs_fat.h"
#include "sdmmc_cmd.h"
#define TAG "m5stack_sdcard"
#define SDCARD_SPI_HOST SPI2_HOST
#define SDCARD_PIN_CS GPIO_NUM_4
#define SDCARD_SPI_FREQUENCY 800000U
#define SDCARD_FORMAT_ON_MOUNT_FAILED false
#define SDCARD_MAX_OPEN_FILES 4
#define SDCARD_ALLOC_UNIT_SIZE (16 * 1024)
#define SDCARD_STATUS_CHECK_ENABLED false
typedef struct {
const char* mount_point;
sdmmc_card_t* card;
} MountData;
/**
* Before we can initialize the sdcard's SPI communications, we have to set all
* other SPI pins on the board high.
* See https://github.com/espressif/esp-idf/issues/1597
* See https://github.com/Xinyuan-LilyGO/T-Deck/blob/master/examples/UnitTest/UnitTest.ino
* @return success result
*/
static bool sdcard_init() {
TT_LOG_D(TAG, "init");
gpio_config_t config = {
.pin_bit_mask = BIT64(GPIO_NUM_4) | BIT64(GPIO_NUM_5),
.mode = GPIO_MODE_OUTPUT,
.pull_up_en = GPIO_PULLUP_DISABLE,
.pull_down_en = GPIO_PULLDOWN_DISABLE,
.intr_type = GPIO_INTR_DISABLE,
};
if (gpio_config(&config) != ESP_OK) {
TT_LOG_E(TAG, "GPIO init failed");
return false;
}
if (gpio_set_level(GPIO_NUM_4, 1) != ESP_OK) {
TT_LOG_E(TAG, "Failed to set board CS pin high");
return false;
}
if (gpio_set_level(GPIO_NUM_5, 1) != ESP_OK) {
TT_LOG_E(TAG, "Failed to set board CS pin high");
return false;
}
return true;
}
static void* sdcard_mount(const char* mount_point) {
TT_LOG_I(TAG, "Mounting %s", mount_point);
esp_vfs_fat_sdmmc_mount_config_t mount_config = {
.format_if_mount_failed = SDCARD_FORMAT_ON_MOUNT_FAILED,
.max_files = SDCARD_MAX_OPEN_FILES,
.allocation_unit_size = SDCARD_ALLOC_UNIT_SIZE,
.disk_status_check_enable = SDCARD_STATUS_CHECK_ENABLED,
.use_one_fat = false
};
sdmmc_card_t* card;
// Init without card detect (CD) and write protect (WD)
sdspi_device_config_t slot_config = SDSPI_DEVICE_CONFIG_DEFAULT();
slot_config.gpio_cs = SDCARD_PIN_CS;
slot_config.host_id = SDCARD_SPI_HOST;
sdmmc_host_t host = SDSPI_HOST_DEFAULT();
host.max_freq_khz = SDCARD_SPI_FREQUENCY;
esp_err_t ret = esp_vfs_fat_sdspi_mount(mount_point, &host, &slot_config, &mount_config, &card);
if (ret != ESP_OK) {
if (ret == 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(ret));
}
return nullptr;
}
auto* data = static_cast<MountData*>(malloc(sizeof(MountData)));
*data = (MountData) {
.mount_point = mount_point,
.card = card,
};
return data;
}
static void* sdcard_init_and_mount(const char* mount_point) {
if (!sdcard_init()) {
TT_LOG_E(TAG, "Failed to set SPI CS pins high. This is a pre-requisite for mounting.");
return NULL;
}
auto* data = static_cast<MountData*>(sdcard_mount(mount_point));
if (data == nullptr) {
TT_LOG_E(TAG, "Mount failed for %s", mount_point);
return nullptr;
}
sdmmc_card_print_info(stdout, data->card);
return data;
}
static void sdcard_unmount(void* context) {
auto* data = static_cast<MountData*>(context);
TT_LOG_I(TAG, "Unmounting %s", data->mount_point);
tt_assert(data != nullptr);
if (esp_vfs_fat_sdcard_unmount(data->mount_point, data->card) != ESP_OK) {
TT_LOG_E(TAG, "Unmount failed for %s", data->mount_point);
}
free(data);
}
static bool sdcard_is_mounted(void* context) {
auto* data = static_cast<MountData*>(context);
return (data != nullptr) && (sdmmc_get_status(data->card) == ESP_OK);
}
extern const tt::hal::sdcard::SdCard m5stack_sdcard = {
.mount = &sdcard_init_and_mount,
.unmount = &sdcard_unmount,
.is_mounted = &sdcard_is_mounted,
.mount_behaviour = tt::hal::sdcard::MountBehaviourAnytime
};

View File

@ -0,0 +1,31 @@
#include "M5stackSdCard.h"
#include "lvgl/LvglSync.h"
#include "hal/SpiSdCard.h"
#include <esp_vfs_fat.h>
#include <sdmmc_cmd.h>
#define SDCARD_PIN_CS GPIO_NUM_4
#define SDCARD_SPI_FREQUENCY 800000U
std::shared_ptr<SdCard> createM5SdCard() {
auto* configuration = new tt::hal::SpiSdCard::Config(
SDCARD_SPI_FREQUENCY,
SDCARD_PIN_CS,
GPIO_NUM_NC,
GPIO_NUM_NC,
GPIO_NUM_NC,
SdCard::MountBehaviourAtBoot,
tt::lvgl::getLvglSyncLockable(),
{
GPIO_NUM_5
}
);
auto* sdcard = (SdCard*) new SpiSdCard(
std::unique_ptr<SpiSdCard::Config>(configuration)
);
return std::shared_ptr<SdCard>(sdcard);
}

View File

@ -0,0 +1,7 @@
#pragma once
#include "hal/SdCard.h"
using namespace tt::hal;
std::shared_ptr<SdCard> createM5SdCard();

View File

@ -5,11 +5,10 @@
#include "src/lv_init.h" #include "src/lv_init.h"
#include "SdlDisplay.h" #include "SdlDisplay.h"
#include "SdlKeyboard.h" #include "SdlKeyboard.h"
#include "SimulatorSdCard.h"
#define TAG "hardware" #define TAG "hardware"
extern const tt::hal::sdcard::SdCard simulatorSdcard;
static bool initBoot() { static bool initBoot() {
lv_init(); lv_init();
lvgl_task_start(); lvgl_task_start();
@ -31,7 +30,7 @@ extern const tt::hal::Configuration hardware = {
.initBoot = initBoot, .initBoot = initBoot,
.createDisplay = createDisplay, .createDisplay = createDisplay,
.createKeyboard = createKeyboard, .createKeyboard = createKeyboard,
.sdcard = &simulatorSdcard, .sdcard = std::make_shared<SimulatorSdCard>(),
.power = simulatorPower, .power = simulatorPower,
.i2c = { .i2c = {
tt::hal::i2c::Configuration { tt::hal::i2c::Configuration {

View File

@ -0,0 +1,26 @@
#pragma once
#include "hal/SdCard.h"
using namespace tt::hal;
class SimulatorSdCard : public SdCard {
private:
State state;
public:
SimulatorSdCard() : SdCard(MountBehaviourAtBoot), state(StateUnmounted) {}
bool mount(const char* mountPath) override {
state = StateMounted;
return true;
}
bool unmount() override {
state = StateUnmounted;
return true;
}
State getState() const override {
return state;
}
};

View File

@ -1,26 +0,0 @@
#include "Tactility.h"
#include "hal/sdcard/Sdcard.h"
static uint32_t fake_handle = 0;
static void* _Nullable sdcard_mount(TT_UNUSED const char* mount_point) {
return &fake_handle;
}
static void* sdcard_init_and_mount(TT_UNUSED const char* mount_point) {
return &fake_handle;
}
static void sdcard_unmount(TT_UNUSED void* context) {
}
static bool sdcard_is_mounted(TT_UNUSED void* context) {
return TT_SCREENSHOT_MODE;
}
extern const tt::hal::sdcard::SdCard simulatorSdcard = {
.mount = &sdcard_init_and_mount,
.unmount = &sdcard_unmount,
.is_mounted = &sdcard_is_mounted,
.mount_behaviour = tt::hal::sdcard::MountBehaviourAtBoot
};

View File

@ -1,4 +1,4 @@
#include "Config.h" #include "YellowConfig.h"
#include "TactilityCore.h" #include "TactilityCore.h"
#include "hal/YellowTouchConstants.h" #include "hal/YellowTouchConstants.h"
#include <driver/spi_common.h> #include <driver/spi_common.h>
@ -6,6 +6,8 @@
#define TAG "twodotfour_bootstrap" #define TAG "twodotfour_bootstrap"
static bool init_i2c() { static bool init_i2c() {
TT_LOG_I(TAG, LOG_MESSAGE_I2C_INIT_START);
const i2c_config_t i2c_conf = { const i2c_config_t i2c_conf = {
.mode = I2C_MODE_MASTER, .mode = I2C_MODE_MASTER,
.sda_io_num = GPIO_NUM_33, .sda_io_num = GPIO_NUM_33,
@ -18,12 +20,12 @@ static bool init_i2c() {
}; };
if (i2c_param_config(TWODOTFOUR_TOUCH_I2C_PORT, &i2c_conf) != ESP_OK) { if (i2c_param_config(TWODOTFOUR_TOUCH_I2C_PORT, &i2c_conf) != ESP_OK) {
TT_LOG_E(TAG, "i2c config failed"); TT_LOG_E(TAG, LOG_MESSAGE_I2C_INIT_CONFIG_FAILED );
return false; return false;
} }
if (i2c_driver_install(TWODOTFOUR_TOUCH_I2C_PORT, i2c_conf.mode, 0, 0, 0) != ESP_OK) { if (i2c_driver_install(TWODOTFOUR_TOUCH_I2C_PORT, i2c_conf.mode, 0, 0, 0) != ESP_OK) {
TT_LOG_E(TAG, "i2c driver install failed"); TT_LOG_E(TAG, LOG_MESSAGE_I2C_INIT_DRIVER_INSTALL_FAILED);
return false; return false;
} }
@ -31,6 +33,8 @@ static bool init_i2c() {
} }
static bool init_spi2() { static bool init_spi2() {
TT_LOG_I(TAG, LOG_MESSAGE_SPI_INIT_START_FMT, SPI2_HOST);
const spi_bus_config_t bus_config = { const spi_bus_config_t bus_config = {
.mosi_io_num = TWODOTFOUR_SPI2_PIN_MOSI, .mosi_io_num = TWODOTFOUR_SPI2_PIN_MOSI,
.miso_io_num = GPIO_NUM_NC, .miso_io_num = GPIO_NUM_NC,
@ -41,7 +45,7 @@ static bool init_spi2() {
}; };
if (spi_bus_initialize(SPI2_HOST, &bus_config, SPI_DMA_CH_AUTO) != ESP_OK) { if (spi_bus_initialize(SPI2_HOST, &bus_config, SPI_DMA_CH_AUTO) != ESP_OK) {
TT_LOG_E(TAG, "SPI bus init failed"); TT_LOG_E(TAG, LOG_MESSAGE_SPI_INIT_FAILED_FMT, SPI2_HOST);
return false; return false;
} }
@ -49,17 +53,26 @@ static bool init_spi2() {
} }
static bool init_spi3() { static bool init_spi3() {
TT_LOG_I(TAG, LOG_MESSAGE_SPI_INIT_START_FMT, SPI3_HOST);
const spi_bus_config_t bus_config = { const spi_bus_config_t bus_config = {
.mosi_io_num = TWODOTFOUR_SPI3_PIN_MOSI, .mosi_io_num = TWODOTFOUR_SPI3_PIN_MOSI,
.miso_io_num = TWODOTFOUR_SPI3_PIN_MISO, .miso_io_num = TWODOTFOUR_SPI3_PIN_MISO,
.sclk_io_num = TWODOTFOUR_SPI3_PIN_SCLK, .sclk_io_num = TWODOTFOUR_SPI3_PIN_SCLK,
.quadwp_io_num = GPIO_NUM_NC, .quadwp_io_num = GPIO_NUM_NC,
.quadhd_io_num = GPIO_NUM_NC, .quadhd_io_num = GPIO_NUM_NC,
.max_transfer_sz = TWODOTFOUR_SPI3_TRANSACTION_LIMIT .data4_io_num = 0,
.data5_io_num = 0,
.data6_io_num = 0,
.data7_io_num = 0,
.max_transfer_sz = TWODOTFOUR_SPI3_TRANSACTION_LIMIT,
.flags = 0,
.isr_cpu_id = ESP_INTR_CPU_AFFINITY_AUTO,
.intr_flags = 0
}; };
if (spi_bus_initialize(SPI3_HOST, &bus_config, SPI_DMA_CH_AUTO) != ESP_OK) { if (spi_bus_initialize(SPI3_HOST, &bus_config, SPI_DMA_CH_AUTO) != ESP_OK) {
TT_LOG_E(TAG, "SPI bus init failed"); TT_LOG_E(TAG, LOG_MESSAGE_SPI_INIT_FAILED_FMT, SPI3_HOST);
return false; return false;
} }
@ -67,23 +80,5 @@ static bool init_spi3() {
} }
bool twodotfour_boot() { bool twodotfour_boot() {
TT_LOG_I(TAG, "Init I2C"); return init_i2c() && init_spi2() && init_spi3();
if (!init_i2c()) {
TT_LOG_E(TAG, "Init I2C failed");
return false;
}
TT_LOG_I(TAG, "Init SPI2");
if (!init_spi2()) {
TT_LOG_E(TAG, "Init SPI2 failed");
return false;
}
TT_LOG_I(TAG, "Init SPI3");
if (!init_spi3()) {
TT_LOG_E(TAG, "Init SPI3 failed");
return false;
}
return true;
} }

View File

@ -1,79 +0,0 @@
#include "hal/sdcard/Sdcard.h"
#include "Check.h"
#include "Log.h"
#include "Config.h"
#include "esp_vfs_fat.h"
#include "sdmmc_cmd.h"
#define TAG "twodotfour_sdcard"
typedef struct {
const char* mount_point;
sdmmc_card_t* card;
} MountData;
static void* sdcard_mount(const char* mount_point) {
TT_LOG_I(TAG, "Mounting %s", mount_point);
esp_vfs_fat_sdmmc_mount_config_t mount_config = {
.format_if_mount_failed = TWODOTFOUR_SDCARD_FORMAT_ON_MOUNT_FAILED,
.max_files = TWODOTFOUR_SDCARD_MAX_OPEN_FILES,
.allocation_unit_size = TWODOTFOUR_SDCARD_ALLOC_UNIT_SIZE,
.disk_status_check_enable = TWODOTFOUR_SDCARD_STATUS_CHECK_ENABLED
};
sdmmc_card_t* card;
// Init without card detect (CD) and write protect (WD)
sdspi_device_config_t slot_config = SDSPI_DEVICE_CONFIG_DEFAULT();
slot_config.gpio_cs = TWODOTFOUR_SDCARD_PIN_CS;
slot_config.host_id = TWODOTFOUR_SDCARD_SPI_HOST;
sdmmc_host_t host = SDSPI_HOST_DEFAULT();
host.max_freq_khz = TWODOTFOUR_SDCARD_SPI_FREQUENCY;
esp_err_t ret = esp_vfs_fat_sdspi_mount(mount_point, &host, &slot_config, &mount_config, &card);
if (ret != ESP_OK) {
if (ret == 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(ret));
}
return nullptr;
}
auto* data = static_cast<MountData*>(malloc(sizeof(MountData)));
*data = (MountData) {
.mount_point = mount_point,
.card = card
};
sdmmc_card_print_info(stdout, data->card);
return data;
}
static void sdcard_unmount(void* context) {
auto* data = static_cast<MountData*>(context);
TT_LOG_I(TAG, "Unmounting %s", data->mount_point);
tt_assert(data != nullptr);
if (esp_vfs_fat_sdcard_unmount(data->mount_point, data->card) != ESP_OK) {
TT_LOG_E(TAG, "Unmount failed for %s", data->mount_point);
}
free(data);
}
static bool sdcard_is_mounted(void* context) {
auto* data = static_cast<MountData*>(context);
return (data != nullptr) && (sdmmc_get_status(data->card) == ESP_OK);
}
extern const tt::hal::sdcard::SdCard twodotfour_sdcard = {
.mount = &sdcard_mount,
.unmount = &sdcard_unmount,
.is_mounted = &sdcard_is_mounted,
.mount_behaviour = tt::hal::sdcard::MountBehaviourAnytime
};

View File

@ -1,16 +1,15 @@
#include "YellowBoard.h" #include "YellowBoard.h"
#include "hal/YellowDisplay.h" #include "hal/YellowDisplay.h"
#include "hal/YellowSdCard.h"
bool twodotfour_lvgl_init(); bool twodotfour_lvgl_init();
bool twodotfour_boot(); bool twodotfour_boot();
extern const tt::hal::sdcard::SdCard twodotfour_sdcard;
const tt::hal::Configuration yellow_board_24inch_cap = { const tt::hal::Configuration yellow_board_24inch_cap = {
.initBoot = &twodotfour_boot, .initBoot = &twodotfour_boot,
.initLvgl = &twodotfour_lvgl_init, .initLvgl = &twodotfour_lvgl_init,
.createDisplay = createDisplay, .createDisplay = createDisplay,
.sdcard = &twodotfour_sdcard, .sdcard = createYellowSdCard(),
.power = nullptr, .power = nullptr,
.i2c = { .i2c = {
tt::hal::i2c::Configuration { tt::hal::i2c::Configuration {

View File

@ -15,12 +15,3 @@
#define TWODOTFOUR_SPI3_PIN_MOSI GPIO_NUM_23 #define TWODOTFOUR_SPI3_PIN_MOSI GPIO_NUM_23
#define TWODOTFOUR_SPI3_PIN_MISO GPIO_NUM_19 #define TWODOTFOUR_SPI3_PIN_MISO GPIO_NUM_19
#define TWODOTFOUR_SPI3_TRANSACTION_LIMIT 8192 // TODO: Determine proper limit #define TWODOTFOUR_SPI3_TRANSACTION_LIMIT 8192 // TODO: Determine proper limit
// SD Card
#define TWODOTFOUR_SDCARD_SPI_HOST SPI3_HOST
#define TWODOTFOUR_SDCARD_PIN_CS GPIO_NUM_5
#define TWODOTFOUR_SDCARD_SPI_FREQUENCY 800000U
#define TWODOTFOUR_SDCARD_FORMAT_ON_MOUNT_FAILED false
#define TWODOTFOUR_SDCARD_MAX_OPEN_FILES 4
#define TWODOTFOUR_SDCARD_ALLOC_UNIT_SIZE (16 * 1024)
#define TWODOTFOUR_SDCARD_STATUS_CHECK_ENABLED false

View File

@ -0,0 +1,31 @@
#include "YellowSdCard.h"
#define TAG "twodotfour_sdcard"
#include "lvgl/LvglSync.h"
#include "hal/SpiSdCard.h"
#define SDCARD_SPI_HOST SPI3_HOST
#define SDCARD_PIN_CS GPIO_NUM_5
#define SDCARD_SPI_FREQUENCY 800000U
std::shared_ptr<SdCard> createYellowSdCard() {
auto* configuration = new tt::hal::SpiSdCard::Config(
SDCARD_SPI_FREQUENCY,
SDCARD_PIN_CS,
GPIO_NUM_NC,
GPIO_NUM_NC,
GPIO_NUM_NC,
SdCard::MountBehaviourAtBoot,
nullptr,
std::vector<gpio_num_t>(),
SDCARD_SPI_HOST
);
auto* sdcard = (SdCard*) new SpiSdCard(
std::unique_ptr<SpiSdCard::Config>(configuration)
);
return std::shared_ptr<SdCard>(sdcard);
}

View File

@ -0,0 +1,8 @@
#pragma once
#include "hal/SdCard.h"
using namespace tt::hal;
std::shared_ptr<SdCard> createYellowSdCard();

View File

@ -36,6 +36,7 @@ cd Libraries/lvgl
find src/ -name '*.h' | cpio -pdm $find_target_dir find src/ -name '*.h' | cpio -pdm $find_target_dir
cd - cd -
cp Libraries/lvgl/lvgl.h $find_target_dir cp Libraries/lvgl/lvgl.h $find_target_dir
cp Libraries/lvgl/lv_version.h $find_target_dir
cp Libraries/lvgl/LICENCE.txt $lvgl_library_path/LICENSE.txt cp Libraries/lvgl/LICENCE.txt $lvgl_library_path/LICENSE.txt
cp Libraries/lvgl_conf/lv_conf_kconfig.h $lvgl_library_path/Include/lv_conf.h cp Libraries/lvgl_conf/lv_conf_kconfig.h $lvgl_library_path/Include/lv_conf.h

View File

@ -1,6 +1,5 @@
# Bugs # Bugs
- I2C Scanner is on M5Stack devices is broken - I2C Scanner is on M5Stack devices is broken
- Fix screenshot app on ESP32: it currently blocks when allocating memory (its cmakelists.txt also needs a fix, see TODO in there)
- WiFi bug: when pressing disconnect while between `WIFI_EVENT_STA_START` and `IP_EVENT_STA_GOT_IP`, then auto-connect becomes active again. - WiFi bug: when pressing disconnect while between `WIFI_EVENT_STA_START` and `IP_EVENT_STA_GOT_IP`, then auto-connect becomes active again.
- ESP32 (CYD) memory issues (or any device without PSRAM): - ESP32 (CYD) memory issues (or any device without PSRAM):
- Boot app doesn't show logo - Boot app doesn't show logo
@ -10,22 +9,21 @@
- When no PSRAM is available, use simplified desktop buttons - When no PSRAM is available, use simplified desktop buttons
- Add statusbar icon for memory pressure. - Add statusbar icon for memory pressure.
- Show error in WiFi screen (e.g. AlertDialog when SPI is not enabled and available memory is below a certain amount) - Show error in WiFi screen (e.g. AlertDialog when SPI is not enabled and available memory is below a certain amount)
- WiFi details "forget" button should be hidden when WiFi credentials are not stores yet. - Clean up static_cast when casting to base class.
# TODOs # TODOs
- Call tt::lvgl::isSyncSet after HAL init and show error (and crash?) when it is not set.
- Create different partitions files for different ESP flash size targets (N4, N8, N16, N32) - Create different partitions files for different ESP flash size targets (N4, N8, N16, N32)
- Rewrite `sdcard` HAL to class
- Attach ELF data to wrapper app (as app data) (check that app state is "running"!) so you can run more than 1 external apps at a time. - Attach ELF data to wrapper app (as app data) (check that app state is "running"!) so you can run more than 1 external apps at a time.
We'll need to keep track of all manifest instances, so that the wrapper can look up the relevant manifest for the relevant callbacks. We'll need to keep track of all manifest instances, so that the wrapper can look up the relevant manifest for the relevant callbacks.
- T-Deck: Clear screen before turning on blacklight - T-Deck: Clear screen before turning on blacklight
- Audio player app - Audio player app
- Audio recording app - Audio recording app
- Files app: file operations: rename, delete, copy, paste (long press?), create folder
- T-Deck: Use knob for UI selection - T-Deck: Use knob for UI selection
- Logging to disk/etc. - Logging to disk/etc.
- Crash monitoring: Keep track of which system phase the app crashed in (e.g. which app in which state) - Crash monitoring: Keep track of which system phase the app crashed in (e.g. which app in which state)
- AppContext's onResult should pass the app id (or launch request id!) that was started, so we can differentiate between multiple types of apps being launched - AppContext's onResult should pass the app id (or launch request id!) that was started, so we can differentiate between multiple types of apps being launched
- Loader: Use main dispatcher instead of Thread, and move API to `tt::app::` - Loader: Use main dispatcher instead of Thread
- Create more unit tests for `tactility-core` and `tactility` (PC-only for now) - Create more unit tests for `tactility-core` and `tactility` (PC-only for now)
- Show a warning screen if firmware encryption or secure boot are off when saving WiFi credentials. - Show a warning screen if firmware encryption or secure boot are off when saving WiFi credentials.
- Show a warning screen when a user plugs in the SD card on a device that only supports mounting at boot. - Show a warning screen when a user plugs in the SD card on a device that only supports mounting at boot.
@ -43,6 +41,10 @@
- Make firmwares available via web serial website - Make firmwares available via web serial website
- If present, use LED to show boot/wifi status - If present, use LED to show boot/wifi status
- T-Deck Power: capacity estimation uses linear voltage curve, but it should use some sort of battery discharge curve. - T-Deck Power: capacity estimation uses linear voltage curve, but it should use some sort of battery discharge curve.
- Statusbar widget to show how much memory is in use?
- Wrapper for Slider that shows "+" and "-" buttons, and also the value in a label.
- Display app: Add toggle to display performance measurement overlay (consider showing FPS in statusbar!)
- Files app: copy/paste actions
# App Ideas # App Ideas
- USB implementation to make device act as mass storage device. - USB implementation to make device act as mass storage device.

View File

@ -1,9 +1,8 @@
#include <stddef.h> #include "tt_app_manifest.h"
#include "TactilityC/app/App.h" #include "tt_lvgl_toolbar.h"
#include "TactilityC/lvgl/Toolbar.h"
/** /**
* Note: LVGL and Tactility methods need to be exposed manually from TactilityC/Source/TactilityC.cpp * Note: LVGL and Tactility methods need to be exposed manually from TactilityC/Source/tt_init.cpp
* Only C is supported for now (C++ symbols fail to link) * Only C is supported for now (C++ symbols fail to link)
*/ */
static void onShow(AppContextHandle context, lv_obj_t* parent) { static void onShow(AppContextHandle context, lv_obj_t* parent) {

View File

@ -5,18 +5,17 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF) set(CMAKE_CXX_EXTENSIONS OFF)
if (DEFINED ENV{ESP_IDF_VERSION}) if (DEFINED ENV{ESP_IDF_VERSION})
file(GLOB_RECURSE SOURCE_FILES Source/*.c*) # TODO: Fix
idf_component_register( idf_component_register(
SRCS ${SOURCE_FILES} SRC_DIRS "Source/"
INCLUDE_DIRS "src/" INCLUDE_DIRS "Source/"
PRIV_INCLUDE_DIRS "Private/"
REQUIRES lvgl REQUIRES lvgl
) )
add_definitions(-DESP_PLATFORM) add_definitions(-DESP_PLATFORM)
else() else()
file(GLOB SOURCES "src/*.c*") file(GLOB SOURCES "Source/*.c*")
file(GLOB HEADERS "src/*.h*") file(GLOB HEADERS "Source/*.h*")
add_library(lv_screenshot STATIC) add_library(lv_screenshot STATIC)
@ -26,8 +25,8 @@ else()
) )
target_include_directories(lv_screenshot target_include_directories(lv_screenshot
PRIVATE private PRIVATE Private
PUBLIC src PUBLIC Source
) )
target_link_libraries(lv_screenshot target_link_libraries(lv_screenshot

View File

@ -1,25 +1,29 @@
#include "lv_screenshot.h" #include "lv_screenshot.h"
#include "save_png.h" #include "save_png.h"
#include "save_bmp.h"
#ifdef __cplusplus
extern "C" {
#endif
static void data_pre_processing(lv_draw_buf_t* snapshot, uint16_t bpp, lv_100ask_screenshot_sv_t screenshot_sv); static void data_pre_processing(lv_draw_buf_t* snapshot, uint16_t bpp, lv_100ask_screenshot_sv_t screenshot_sv);
bool lv_screenshot_create(lv_obj_t* obj, lv_color_format_t cf, lv_100ask_screenshot_sv_t screenshot_sv, const char* filename) { bool lv_screenshot_create(lv_obj_t* obj, lv_100ask_screenshot_sv_t screenshot_sv, const char* filename) {
lv_draw_buf_t* snapshot = lv_snapshot_take(obj, cf); lv_draw_buf_t* snapshot = lv_snapshot_take(obj, LV_COLOR_FORMAT_RGB888);
if (snapshot) { if (snapshot) {
data_pre_processing(snapshot, LV_COLOR_DEPTH, screenshot_sv); bool success = false;
if (screenshot_sv == LV_100ASK_SCREENSHOT_SV_PNG) { if (screenshot_sv == LV_100ASK_SCREENSHOT_SV_PNG) {
if (LV_COLOR_DEPTH == 16) { data_pre_processing(snapshot, 24, screenshot_sv);
lv_screenshot_save_png_file(snapshot->data, snapshot->header.w, snapshot->header.h, 24, filename); success = lv_screenshot_save_png_file(snapshot->data, snapshot->header.w, snapshot->header.h, 24, filename);
} else if (LV_COLOR_DEPTH == 32) { } else if (screenshot_sv == LV_100ASK_SCREENSHOT_SV_BMP) {
lv_screenshot_save_png_file(snapshot->data, snapshot->header.w, snapshot->header.h, 32, filename); data_pre_processing(snapshot, 24, screenshot_sv);
} success = lve_screenshot_save_bmp_file(snapshot->data, snapshot->header.w, snapshot->header.h, 24, filename);
} }
lv_draw_buf_destroy(snapshot); lv_draw_buf_destroy(snapshot);
return true; return success;
} }
return false; return false;
@ -45,16 +49,21 @@ static void data_pre_processing(lv_draw_buf_t* snapshot, uint16_t bpp, lv_100ask
count += 3; count += 3;
} }
} }
} else if ((screenshot_sv == LV_100ASK_SCREENSHOT_SV_PNG) && (bpp == 32)) { } else if ((screenshot_sv == LV_100ASK_SCREENSHOT_SV_PNG) && (bpp == 32 || bpp == 24)) {
uint8_t tmp_data = 0; uint8_t tmp_data = 0;
uint32_t count = 0; uint32_t count = 0;
uint32_t pixel_byte_gap = bpp / 8;
for (int w = 0; w < snapshot->header.w; w++) { for (int w = 0; w < snapshot->header.w; w++) {
for (int h = 0; h < snapshot->header.h; h++) { for (int h = 0; h < snapshot->header.h; h++) {
tmp_data = *(snapshot->data + count); tmp_data = *(snapshot->data + count);
*(uint8_t*)(snapshot->data + count) = *(snapshot->data + count + 2); *(uint8_t*)(snapshot->data + count) = *(snapshot->data + count + 2);
*(uint8_t*)(snapshot->data + count + 2) = tmp_data; *(uint8_t*)(snapshot->data + count + 2) = tmp_data;
count += 4; count += pixel_byte_gap;
} }
} }
} }
} }
#ifdef __cplusplus
}
#endif

View File

@ -1,8 +1,6 @@
#pragma once #pragma once
#include "lvgl.h" #include "lvgl.h"
#include <stdbool.h>
#ifdef __cplusplus #ifdef __cplusplus
extern "C" { extern "C" {
@ -14,7 +12,7 @@ typedef enum {
LV_100ASK_SCREENSHOT_SV_LAST LV_100ASK_SCREENSHOT_SV_LAST
} lv_100ask_screenshot_sv_t; } lv_100ask_screenshot_sv_t;
bool lv_screenshot_create(lv_obj_t* obj, lv_color_format_t cf, lv_100ask_screenshot_sv_t screenshot_sv, const char* filename); bool lv_screenshot_create(lv_obj_t* obj, lv_100ask_screenshot_sv_t screenshot_sv, const char* filename);
#ifdef __cplusplus #ifdef __cplusplus
} /*extern "C"*/ } /*extern "C"*/

View File

@ -0,0 +1,96 @@
#include <memory.h>
#include <stdint.h>
#include <stdbool.h>
#include "lvgl.h"
typedef struct tagBITMAPFILEHEADER {
uint16_t bfType;
uint32_t bfSize;
uint16_t bfReserved1;
uint16_t bfReserved2;
uint32_t bfOffBits;
} __attribute__((packed)) BITMAPFILEHEADER, *PBITMAPFILEHEADER;
typedef struct tagBITMAPINFOHEADER {
uint32_t biSize;
uint32_t biwidth;
uint32_t biheight;
uint16_t biPlanes;
uint16_t biBitCount;
uint32_t biCompression;
uint32_t biSizeImage;
uint32_t biXPelsPerMeter;
uint32_t biYPelsPerMeter;
uint32_t biClrUsed;
uint32_t biClrImportant;
} __attribute__((packed)) BITMAPINFOHEADER, *PBITMAPINFOHEADER;
typedef struct tagRGBQUAD {
uint8_t rgbBlue;
uint8_t rgbGreen;
uint8_t rgbRed;
uint8_t rgbReserved;
} __attribute__((packed)) RGBQUAD;
bool lve_screenshot_save_bmp_file(const uint8_t* image, uint32_t w, uint32_t h, uint32_t bpp, const char* filename) {
BITMAPFILEHEADER tBmpFileHead;
BITMAPINFOHEADER tBmpInfoHead;
uint32_t dwSize;
uint32_t bw;
lv_fs_file_t f;
memset(&tBmpFileHead, 0, sizeof(BITMAPFILEHEADER));
memset(&tBmpInfoHead, 0, sizeof(BITMAPINFOHEADER));
lv_fs_res_t res = lv_fs_open(&f, filename, LV_FS_MODE_WR);
if (res != LV_FS_RES_OK) {
LV_LOG_USER("Can't create output file %s", filename);
return false;
}
tBmpFileHead.bfType = 0x4d42;
tBmpFileHead.bfSize = 0x36 + w * h * (bpp / 8);
tBmpFileHead.bfOffBits = 0x00000036;
tBmpInfoHead.biSize = 0x00000028;
tBmpInfoHead.biwidth = w;
tBmpInfoHead.biheight = h;
tBmpInfoHead.biPlanes = 0x0001;
tBmpInfoHead.biBitCount = bpp;
tBmpInfoHead.biCompression = 0;
tBmpInfoHead.biSizeImage = w * h * (bpp / 8);
tBmpInfoHead.biXPelsPerMeter = 0;
tBmpInfoHead.biYPelsPerMeter = 0;
tBmpInfoHead.biClrUsed = 0;
tBmpInfoHead.biClrImportant = 0;
res = lv_fs_write(&f, &tBmpFileHead, sizeof(tBmpFileHead), &bw);
if (bw != sizeof(tBmpFileHead)) {
LV_LOG_USER("Can't write BMP File Head to %s", filename);
return false;
}
res = lv_fs_write(&f, &tBmpInfoHead, sizeof(tBmpInfoHead), &bw);
if (bw != sizeof(tBmpInfoHead)) {
LV_LOG_USER("Can't write BMP File Info Head to %s", filename);
return false;
}
dwSize = w * bpp / 8;
const uint8_t* pPos = image + (h - 1) * dwSize;
while (pPos >= image) {
res = lv_fs_write(&f, pPos, dwSize, &bw);
if (bw != dwSize) {
LV_LOG_USER("Can't write date to BMP File %s", filename);
return false;
}
pPos -= dwSize;
}
lv_fs_close(&f);
return true;
}

View File

@ -1,11 +1,11 @@
#include "save_png.h"
#include "src/libs/lodepng/lodepng.h" #include "src/libs/lodepng/lodepng.h"
bool lv_screenshot_save_png_file(const uint8_t* image, uint32_t w, uint32_t h, uint32_t bpp, const char* filename) { bool lv_screenshot_save_png_file(const uint8_t* image, uint32_t w, uint32_t h, uint32_t bpp, const char* filename) {
if (bpp == 32) { if (bpp == 32) {
return lodepng_encode32_file(filename, image, w, h); return lodepng_encode32_file(filename, image, w, h) == 0;
} else if (bpp == 24) { } else if (bpp == 24) {
return lodepng_encode24_file(filename, image, w, h); return lodepng_encode24_file(filename, image, w, h) == 0;
} } else {
return false; return false;
}
} }

View File

@ -2,6 +2,7 @@
#include "app/AppManifest.h" #include "app/AppManifest.h"
#include "app/AppInstance.h" #include "app/AppInstance.h"
#include "EventFlag.h"
#include "MessageQueue.h" #include "MessageQueue.h"
#include "Pubsub.h" #include "Pubsub.h"
#include "Thread.h" #include "Thread.h"
@ -10,6 +11,7 @@
#include "RtosCompatSemaphore.h" #include "RtosCompatSemaphore.h"
#include <stack> #include <stack>
#include <utility> #include <utility>
#include <DispatcherThread.h>
namespace tt::service::loader { namespace tt::service::loader {
@ -53,98 +55,46 @@ typedef struct {
// region LoaderMessage // region LoaderMessage
typedef enum {
LoaderMessageTypeNone,
LoaderMessageTypeAppStart,
LoaderMessageTypeAppStop,
LoaderMessageTypeServiceStop,
} LoaderMessageType;
class LoaderMessageAppStart { class LoaderMessageAppStart {
public: public:
// This lock blocks anyone from starting an app as long
// as an app is already running via loader_start()
// This lock's lifecycle is not owned by this class.
std::shared_ptr<EventFlag> api_lock = std::make_shared<EventFlag>();
std::string id; std::string id;
std::shared_ptr<const Bundle> _Nullable parameters; std::shared_ptr<const Bundle> _Nullable parameters;
LoaderMessageAppStart() = default; LoaderMessageAppStart() = default;
LoaderMessageAppStart(LoaderMessageAppStart& other) :
api_lock(other.api_lock),
id(other.id),
parameters(other.parameters) {}
LoaderMessageAppStart(const std::string& id, std::shared_ptr<const Bundle> parameters) : LoaderMessageAppStart(const std::string& id, std::shared_ptr<const Bundle> parameters) :
id(id), id(id),
parameters(std::move(parameters)) parameters(std::move(parameters))
{} {}
};
typedef struct { ~LoaderMessageAppStart() = default;
LoaderStatus value;
} LoaderMessageLoaderStatusResult;
typedef struct { std::shared_ptr<EventFlag> getApiLockEventFlag() { return api_lock; }
bool value;
} LoaderMessageBoolResult;
class LoaderMessage { uint32_t getApiLockEventFlagValue() { return 1; }
public:
// This lock blocks anyone from starting an app as long
// as an app is already running via loader_start()
// This lock's lifecycle is not owned by this class.
EventFlag* _Nullable api_lock;
LoaderMessageType type;
struct { void onProcessed() {
union { api_lock->set(1);
// TODO: Convert to smart pointer
const LoaderMessageAppStart* start;
};
} payload;
struct {
union {
LoaderMessageLoaderStatusResult status_value;
LoaderMessageBoolResult bool_value;
void* raw_value;
};
} result;
LoaderMessage() {
api_lock = nullptr;
type = LoaderMessageTypeNone;
payload = { .start = nullptr };
result = { .raw_value = nullptr };
}
LoaderMessage(const LoaderMessageAppStart* start, const LoaderMessageLoaderStatusResult& statusResult) {
api_lock = nullptr;
type = LoaderMessageTypeAppStart;
payload.start = start;
result.status_value = statusResult;
}
LoaderMessage(LoaderMessageType messageType) {
api_lock = nullptr;
type = messageType;
payload = { .start = nullptr };
result = { .raw_value = nullptr };
}
void setApiLock(EventFlag* eventFlag) {
api_lock = eventFlag;
}
void cleanup() {
if (type == LoaderMessageTypeAppStart) {
delete payload.start;
}
} }
}; };
// endregion LoaderMessage // endregion LoaderMessage
struct Loader { struct Loader {
Thread* thread;
std::shared_ptr<PubSub> pubsub_internal = std::make_shared<PubSub>(); std::shared_ptr<PubSub> pubsub_internal = std::make_shared<PubSub>();
std::shared_ptr<PubSub> pubsub_external = std::make_shared<PubSub>(); std::shared_ptr<PubSub> pubsub_external = std::make_shared<PubSub>();
MessageQueue queue = MessageQueue(2, sizeof(LoaderMessage)); // 2 entries, so you can stop the current app while starting a new one without blocking
Mutex mutex = Mutex(Mutex::TypeRecursive); Mutex mutex = Mutex(Mutex::TypeRecursive);
std::stack<app::AppInstance*> app_stack; std::stack<app::AppInstance*> app_stack;
std::unique_ptr<DispatcherThread> dispatcherThread = std::make_unique<DispatcherThread>("loader_dispatcher", 6144); // Files app requires ~5k
}; };
} // namespace } // namespace

View File

@ -17,19 +17,19 @@ static const Configuration* config_instance = nullptr;
namespace service { namespace service {
namespace gui { extern const ServiceManifest manifest; } namespace gui { extern const ServiceManifest manifest; }
namespace loader { extern const ServiceManifest manifest; } namespace loader { extern const ServiceManifest manifest; }
#ifndef ESP_PLATFORM // Screenshots don't work yet on ESP32 namespace statusbar { extern const ServiceManifest manifest; }
#if TT_FEATURE_SCREENSHOT_ENABLED
namespace screenshot { extern const ServiceManifest manifest; } namespace screenshot { extern const ServiceManifest manifest; }
#endif #endif
namespace statusbar { extern const ServiceManifest manifest; }
} }
static const std::vector<const service::ServiceManifest*> system_services = { static const std::vector<const service::ServiceManifest*> system_services = {
&service::loader::manifest, &service::loader::manifest,
&service::gui::manifest, // depends on loader service &service::gui::manifest, // depends on loader service
#ifndef ESP_PLATFORM // Screenshots don't work yet on ESP32 &service::statusbar::manifest,
&service::screenshot::manifest, #if TT_FEATURE_SCREENSHOT_ENABLED
&service::screenshot::manifest
#endif #endif
&service::statusbar::manifest
}; };
// endregion // endregion
@ -44,23 +44,24 @@ namespace app {
namespace files { extern const AppManifest manifest; } namespace files { extern const AppManifest manifest; }
namespace gpio { extern const AppManifest manifest; } namespace gpio { extern const AppManifest manifest; }
namespace imageviewer { extern const AppManifest manifest; } namespace imageviewer { extern const AppManifest manifest; }
namespace screenshot { extern const AppManifest manifest; }
namespace settings { extern const AppManifest manifest; }
namespace display { extern const AppManifest manifest; } namespace display { extern const AppManifest manifest; }
namespace i2cscanner { extern const AppManifest manifest; } namespace i2cscanner { extern const AppManifest manifest; }
namespace i2csettings { extern const AppManifest manifest; } namespace i2csettings { extern const AppManifest manifest; }
namespace inputdialog { extern const AppManifest manifest; }
namespace power { extern const AppManifest manifest; } namespace power { extern const AppManifest manifest; }
namespace selectiondialog { extern const AppManifest manifest; } namespace selectiondialog { extern const AppManifest manifest; }
namespace settings { extern const AppManifest manifest; }
namespace systeminfo { extern const AppManifest manifest; } namespace systeminfo { extern const AppManifest manifest; }
namespace textviewer { extern const AppManifest manifest; } namespace textviewer { extern const AppManifest manifest; }
namespace wifiapsettings { extern const AppManifest manifest; } namespace wifiapsettings { extern const AppManifest manifest; }
namespace wificonnect { extern const AppManifest manifest; } namespace wificonnect { extern const AppManifest manifest; }
namespace wifimanage { extern const AppManifest manifest; } namespace wifimanage { extern const AppManifest manifest; }
#if TT_FEATURE_SCREENSHOT_ENABLED
namespace screenshot { extern const AppManifest manifest; }
#endif
#ifdef ESP_PLATFORM #ifdef ESP_PLATFORM
extern const AppManifest elfWrapperManifest; extern const AppManifest elfWrapperManifest;
namespace crashdiagnostics { extern const AppManifest manifest; } namespace crashdiagnostics { extern const AppManifest manifest; }
#else
namespace app::screenshot { extern const AppManifest manifest; }
#endif #endif
} }
@ -77,6 +78,7 @@ static const std::vector<const app::AppManifest*> system_apps = {
&app::gpio::manifest, &app::gpio::manifest,
&app::i2cscanner::manifest, &app::i2cscanner::manifest,
&app::i2csettings::manifest, &app::i2csettings::manifest,
&app::inputdialog::manifest,
&app::imageviewer::manifest, &app::imageviewer::manifest,
&app::settings::manifest, &app::settings::manifest,
&app::selectiondialog::manifest, &app::selectiondialog::manifest,
@ -85,11 +87,12 @@ static const std::vector<const app::AppManifest*> system_apps = {
&app::wifiapsettings::manifest, &app::wifiapsettings::manifest,
&app::wificonnect::manifest, &app::wificonnect::manifest,
&app::wifimanage::manifest, &app::wifimanage::manifest,
#if TT_FEATURE_SCREENSHOT_ENABLED
&app::screenshot::manifest,
#endif
#ifdef ESP_PLATFORM #ifdef ESP_PLATFORM
&app::crashdiagnostics::manifest, &app::crashdiagnostics::manifest,
&app::elfWrapperManifest, // For hot-loading ELF apps &app::elfWrapperManifest, // For hot-loading ELF apps
#else
&app::screenshot::manifest, // Screenshots don't work yet on ESP32
#endif #endif
}; };
@ -166,12 +169,7 @@ void run(const Configuration& config) {
register_user_apps(config.apps); register_user_apps(config.apps);
TT_LOG_I(TAG, "init starting desktop app"); TT_LOG_I(TAG, "init starting desktop app");
service::loader::startApp(app::boot::manifest.id, true); service::loader::startApp(app::boot::manifest.id);
if (config.autoStartAppId) {
TT_LOG_I(TAG, "init auto-starting %s", config.autoStartAppId);
service::loader::startApp(config.autoStartAppId, true);
}
TT_LOG_I(TAG, "init complete"); TT_LOG_I(TAG, "init complete");

View File

@ -1,7 +1,17 @@
#pragma once #pragma once
#ifdef ESP_PLATFORM
#include "sdkconfig.h"
#endif
#include "TactilityHeadlessConfig.h" #include "TactilityHeadlessConfig.h"
#define TT_CONFIG_APPS_LIMIT 32 #define TT_CONFIG_APPS_LIMIT 32
#define TT_CONFIG_FORCE_ONSCREEN_KEYBOARD false // for development/debug purposes #define TT_CONFIG_FORCE_ONSCREEN_KEYBOARD false // for development/debug purposes
#define TT_SCREENSHOT_MODE false // for taking screenshots (e.g. forces SD card presence and Files tree on simulator) #define TT_SCREENSHOT_MODE false // for taking screenshots (e.g. forces SD card presence and Files tree on simulator)
#ifdef ESP_PLATFORM
#define TT_FEATURE_SCREENSHOT_ENABLED (CONFIG_LV_USE_SNAPSHOT == 1 && CONFIG_SPIRAM_USE_MALLOC == 1)
#else // Sim
#define TT_FEATURE_SCREENSHOT_ENABLED true
#endif

View File

@ -16,13 +16,13 @@ static size_t elfManifestSetCount = 0;
std::unique_ptr<uint8_t[]> elfFileData; std::unique_ptr<uint8_t[]> elfFileData;
esp_elf_t elf; esp_elf_t elf;
bool startElfApp(const char* filePath) { bool startElfApp(const std::string& filePath) {
TT_LOG_I(TAG, "Starting ELF %s", filePath); TT_LOG_I(TAG, "Starting ELF %s", filePath.c_str());
assert(elfFileData == nullptr); assert(elfFileData == nullptr);
size_t size = 0; size_t size = 0;
elfFileData = file::readBinary(filePath, size); elfFileData = file::readBinary(filePath.c_str(), size);
if (elfFileData == nullptr) { if (elfFileData == nullptr) {
return false; return false;
} }

View File

@ -6,7 +6,7 @@
namespace tt::app { namespace tt::app {
bool startElfApp(const char* filePath); bool startElfApp(const std::string& filePath);
void setElfAppManifest(const AppManifest& manifest); void setElfAppManifest(const AppManifest& manifest);

View File

@ -49,7 +49,6 @@ static std::string getTitleParameter(std::shared_ptr<const Bundle> bundle) {
static void onButtonClicked(lv_event_t* e) { static void onButtonClicked(lv_event_t* e) {
lv_event_code_t code = lv_event_get_code(e); lv_event_code_t code = lv_event_get_code(e);
if (code == LV_EVENT_CLICKED) {
auto index = reinterpret_cast<std::size_t>(lv_event_get_user_data(e)); auto index = reinterpret_cast<std::size_t>(lv_event_get_user_data(e));
TT_LOG_I(TAG, "Selected item at index %d", index); TT_LOG_I(TAG, "Selected item at index %d", index);
tt::app::AppContext* app = service::loader::getCurrentApp(); tt::app::AppContext* app = service::loader::getCurrentApp();
@ -57,7 +56,6 @@ static void onButtonClicked(lv_event_t* e) {
setResultIndex(bundle, (int32_t)index); setResultIndex(bundle, (int32_t)index);
app->setResult(app::ResultOk, bundle); app->setResult(app::ResultOk, bundle);
service::loader::stopApp(); service::loader::stopApp();
}
} }
static void createButton(lv_obj_t* parent, const std::string& text, size_t index) { static void createButton(lv_obj_t* parent, const std::string& text, size_t index) {
@ -65,7 +63,7 @@ static void createButton(lv_obj_t* parent, const std::string& text, size_t index
lv_obj_t* button_label = lv_label_create(button); lv_obj_t* button_label = lv_label_create(button);
lv_obj_align(button_label, LV_ALIGN_CENTER, 0, 0); lv_obj_align(button_label, LV_ALIGN_CENTER, 0, 0);
lv_label_set_text(button_label, text.c_str()); lv_label_set_text(button_label, text.c_str());
lv_obj_add_event_cb(button, &onButtonClicked, LV_EVENT_CLICKED, (void*)index); lv_obj_add_event_cb(button, &onButtonClicked, LV_EVENT_SHORT_CLICKED, (void*)index);
} }
static void onShow(AppContext& app, lv_obj_t* parent) { static void onShow(AppContext& app, lv_obj_t* parent) {
@ -78,6 +76,7 @@ static void onShow(AppContext& app, lv_obj_t* parent) {
lv_obj_t* message_label = lv_label_create(parent); lv_obj_t* message_label = lv_label_create(parent);
lv_obj_align(message_label, LV_ALIGN_CENTER, 0, 0); lv_obj_align(message_label, LV_ALIGN_CENTER, 0, 0);
lv_obj_set_width(message_label, LV_PCT(80));
std::string message; std::string message;
if (parameters->optString(PARAMETER_BUNDLE_KEY_MESSAGE, message)) { if (parameters->optString(PARAMETER_BUNDLE_KEY_MESSAGE, message)) {

View File

@ -9,11 +9,8 @@
namespace tt::app::applist { namespace tt::app::applist {
static void onAppPressed(lv_event_t* e) { static void onAppPressed(lv_event_t* e) {
lv_event_code_t code = lv_event_get_code(e);
if (code == LV_EVENT_CLICKED) {
const auto* manifest = static_cast<const AppManifest*>(lv_event_get_user_data(e)); const auto* manifest = static_cast<const AppManifest*>(lv_event_get_user_data(e));
service::loader::startApp(manifest->id, false); service::loader::startApp(manifest->id, false);
}
} }
static void createAppWidget(const AppManifest* manifest, void* parent) { static void createAppWidget(const AppManifest* manifest, void* parent) {
@ -21,7 +18,7 @@ static void createAppWidget(const AppManifest* manifest, void* parent) {
auto* list = reinterpret_cast<lv_obj_t*>(parent); auto* list = reinterpret_cast<lv_obj_t*>(parent);
const void* icon = !manifest->icon.empty() ? manifest->icon.c_str() : TT_ASSETS_APP_ICON_FALLBACK; const void* icon = !manifest->icon.empty() ? manifest->icon.c_str() : TT_ASSETS_APP_ICON_FALLBACK;
lv_obj_t* btn = lv_list_add_button(list, icon, manifest->name.c_str()); lv_obj_t* btn = lv_list_add_button(list, icon, manifest->name.c_str());
lv_obj_add_event_cb(btn, &onAppPressed, LV_EVENT_CLICKED, (void*)manifest); lv_obj_add_event_cb(btn, &onAppPressed, LV_EVENT_SHORT_CLICKED, (void*)manifest);
} }
static void onShow(TT_UNUSED AppContext& app, lv_obj_t* parent) { static void onShow(TT_UNUSED AppContext& app, lv_obj_t* parent) {

View File

@ -4,29 +4,33 @@
#include "app/AppContext.h" #include "app/AppContext.h"
#include "app/display/DisplaySettings.h" #include "app/display/DisplaySettings.h"
#include "hal/Display.h" #include "hal/Display.h"
#include "kernel/PanicHandler.h"
#include "service/loader/Loader.h" #include "service/loader/Loader.h"
#include "lvgl/Style.h" #include "lvgl/Style.h"
#include "lvgl.h" #include "lvgl.h"
#include "Tactility.h"
#ifdef ESP_PLATFORM #ifdef ESP_PLATFORM
#include "kernel/PanicHandler.h"
#include "sdkconfig.h" #include "sdkconfig.h"
#else #else
#define CONFIG_TT_SPLASH_DURATION 0 #define CONFIG_TT_SPLASH_DURATION 0
#endif #endif
#define TAG "Boot"
namespace tt::app::boot { namespace tt::app::boot {
static int32_t threadCallback(void* context); static int32_t bootThreadCallback(void* context);
static void startNextApp();
struct Data { struct Data {
Data() : thread("boot", 4096, threadCallback, this) {} Data() : thread("boot", 4096, bootThreadCallback, this) {}
Thread thread; Thread thread;
}; };
static int32_t threadCallback(TT_UNUSED void* context) { static int32_t bootThreadCallback(TT_UNUSED void* context) {
TickType_t start_time = tt::kernel::getTicks(); TickType_t start_time = tt::kernel::getTicks();
auto* lvgl_display = lv_display_get_default(); auto* lvgl_display = lv_display_get_default();
@ -46,18 +50,33 @@ static int32_t threadCallback(TT_UNUSED void* context) {
} }
tt::service::loader::stopApp(); tt::service::loader::stopApp();
startNextApp();
return 0;
}
static void startNextApp() {
auto config = tt::getConfiguration();
std::string next_app;
if (config->autoStartAppId) {
TT_LOG_I(TAG, "init auto-starting %s", config->autoStartAppId);
next_app = config->autoStartAppId;
} else {
next_app = "Desktop";
}
#ifdef ESP_PLATFORM #ifdef ESP_PLATFORM
esp_reset_reason_t reason = esp_reset_reason(); esp_reset_reason_t reason = esp_reset_reason();
if (reason == ESP_RST_PANIC) { if (reason == ESP_RST_PANIC) {
tt::service::loader::startApp("CrashDiagnostics"); tt::service::loader::startApp("CrashDiagnostics");
} else { } else {
tt::service::loader::startApp("Desktop"); tt::service::loader::startApp(next_app);
} }
#else #else
tt::service::loader::startApp("Desktop"); tt::service::loader::startApp(next_app);
#endif #endif
return 0;
} }
static void onShow(TT_UNUSED AppContext& app, lv_obj_t* parent) { static void onShow(TT_UNUSED AppContext& app, lv_obj_t* parent) {

View File

@ -21,7 +21,7 @@ static void onShow(TT_UNUSED AppContext& app, lv_obj_t* parent) {
auto* display = lv_obj_get_display(parent); auto* display = lv_obj_get_display(parent);
int32_t parent_height = lv_display_get_vertical_resolution(display) - STATUSBAR_HEIGHT; int32_t parent_height = lv_display_get_vertical_resolution(display) - STATUSBAR_HEIGHT;
lv_obj_add_event_cb(parent, onContinuePressed, LV_EVENT_CLICKED, nullptr); lv_obj_add_event_cb(parent, onContinuePressed, LV_EVENT_SHORT_CLICKED, nullptr);
auto* top_label = lv_label_create(parent); auto* top_label = lv_label_create(parent);
lv_label_set_text(top_label, "Oops! We've crashed ..."); // TODO: Funny messages lv_label_set_text(top_label, "Oops! We've crashed ..."); // TODO: Funny messages
lv_obj_align(top_label, LV_ALIGN_TOP_MID, 0, 2); lv_obj_align(top_label, LV_ALIGN_TOP_MID, 0, 2);

View File

@ -28,7 +28,7 @@ static lv_obj_t* createAppButton(lv_obj_t* parent, const char* title, const char
auto* button_image = lv_image_create(apps_button); auto* button_image = lv_image_create(apps_button);
lv_image_set_src(button_image, imageFile); lv_image_set_src(button_image, imageFile);
lv_obj_add_event_cb(apps_button, onAppPressed, LV_EVENT_CLICKED, (void*)appId); lv_obj_add_event_cb(apps_button, onAppPressed, LV_EVENT_SHORT_CLICKED, (void*)appId);
lv_obj_set_style_image_recolor(button_image, lv_theme_get_color_primary(parent), 0); lv_obj_set_style_image_recolor(button_image, lv_theme_get_color_primary(parent), 0);
lv_obj_set_style_image_recolor_opa(button_image, LV_OPA_COVER, 0); lv_obj_set_style_image_recolor_opa(button_image, LV_OPA_COVER, 0);

View File

@ -1,97 +1,99 @@
#include "FileUtils.h" #include "FileUtils.h"
#include "TactilityCore.h" #include "TactilityCore.h"
#include <cstdlib>
#include <cstring> #include <cstring>
#include <bits/stdc++.h>
namespace tt::app::files { namespace tt::app::files {
#define TAG "file_utils" #define TAG "file_utils"
#define SCANDIR_LIMIT 128 std::string getChildPath(const std::string& basePath, const std::string& childPath) {
// Postfix with "/" when the current path isn't "/"
if (basePath.length() != 1) {
return basePath + "/" + childPath;
} else {
return "/" + childPath;
}
}
int dirent_filter_dot_entries(const struct dirent* entry) { int dirent_filter_dot_entries(const struct dirent* entry) {
return (strcmp(entry->d_name, "..") == 0 || strcmp(entry->d_name, ".") == 0) ? -1 : 0; return (strcmp(entry->d_name, "..") == 0 || strcmp(entry->d_name, ".") == 0) ? -1 : 0;
} }
int dirent_sort_alpha_and_type(const struct dirent** left, const struct dirent** right) { bool dirent_sort_alpha_and_type(const struct dirent& left, const struct dirent& right) {
bool left_is_dir = (*left)->d_type == TT_DT_DIR || (*left)->d_type == TT_DT_CHR; bool left_is_dir = left.d_type == TT_DT_DIR || left.d_type == TT_DT_CHR;
bool right_is_dir = (*right)->d_type == TT_DT_DIR || (*right)->d_type == TT_DT_CHR; bool right_is_dir = right.d_type == TT_DT_DIR || right.d_type == TT_DT_CHR;
if (left_is_dir == right_is_dir) { if (left_is_dir == right_is_dir) {
return strcmp((*left)->d_name, (*right)->d_name); return strcmp(left.d_name, right.d_name) < 0;
} else { } else {
return (left_is_dir < right_is_dir) ? 1 : -1; return left_is_dir > right_is_dir;
} }
} }
int dirent_sort_alpha(const struct dirent** left, const struct dirent** right) {
return strcmp((*left)->d_name, (*right)->d_name);
}
int scandir( int scandir(
const char* path, const std::string& path,
struct dirent*** output, std::vector<dirent>& outList,
ScandirFilter _Nullable filter, ScandirFilter _Nullable filterMethod,
ScandirSort _Nullable sort ScandirSort _Nullable sortMethod
) { ) {
DIR* dir = opendir(path); DIR* dir = opendir(path.c_str());
if (dir == nullptr) { if (dir == nullptr) {
TT_LOG_E(TAG, "Failed to open dir %s", path); TT_LOG_E(TAG, "Failed to open dir %s", path.c_str());
return -1; return -1;
} }
*output = static_cast<dirent**>(malloc(sizeof(void*) * SCANDIR_LIMIT));
if (*output == nullptr) {
TT_LOG_E(TAG, "Out of memory");
closedir(dir);
return -1;
}
struct dirent** dirent_array = *output;
int next_dirent_index = 0;
struct dirent* current_entry; struct dirent* current_entry;
bool out_of_memory = false;
while ((current_entry = readdir(dir)) != nullptr) { while ((current_entry = readdir(dir)) != nullptr) {
if (filter(current_entry) == 0) { if (filterMethod(current_entry) == 0) {
dirent_array[next_dirent_index] = static_cast<dirent*>(malloc(sizeof(struct dirent))); outList.push_back(*current_entry);
if (dirent_array[next_dirent_index] != nullptr) {
memcpy(dirent_array[next_dirent_index], current_entry, sizeof(struct dirent));
next_dirent_index++;
if (next_dirent_index >= SCANDIR_LIMIT) {
TT_LOG_E(TAG, "Directory has more than %d files", SCANDIR_LIMIT);
break;
}
} else {
TT_LOG_E(TAG, "Alloc failed. Aborting and cleaning up.");
out_of_memory = true;
break;
}
}
}
// Out-of-memory clean-up
if (out_of_memory && next_dirent_index > 0) {
for (int i = 0; i < next_dirent_index; ++i) {
TT_LOG_I(TAG, "Cleanup item %d", i);
free(dirent_array[i]);
}
TT_LOG_I(TAG, "Free");
free(*output);
closedir(dir);
return -1;
// Empty directory
} else if (next_dirent_index == 0) {
free(*output);
*output = nullptr;
} else {
if (sort) {
qsort(dirent_array, next_dirent_index, sizeof(struct dirent*), (__compar_fn_t)sort);
} }
} }
closedir(dir); closedir(dir);
return next_dirent_index;
if (sortMethod != nullptr) {
sort(outList.begin(), outList.end(), sortMethod);
}
return (int)outList.size();
}; };
bool isSupportedExecutableFile(const std::string& filename) {
#ifdef ESP_PLATFORM
// Currently only the PNG library is built into Tactility
return filename.ends_with(".elf");
#else
return false;
#endif
}
template <typename T>
std::basic_string<T> lowercase(const std::basic_string<T>& input) {
std::basic_string<T> output = input;
std::transform(
output.begin(),
output.end(),
output.begin(),
[](const T character) { return static_cast<T>(std::tolower(character)); }
);
return std::move(output);
}
bool isSupportedImageFile(const std::string& filename) {
// Currently only the PNG library is built into Tactility
return lowercase(filename).ends_with(".png");
}
bool isSupportedTextFile(const std::string& filename) {
std::string filename_lower = lowercase(filename);
return filename_lower.ends_with(".txt") ||
filename_lower.ends_with(".ini") ||
filename_lower.ends_with(".json") ||
filename_lower.ends_with(".yaml") ||
filename_lower.ends_with(".yml") ||
filename_lower.ends_with(".lua") ||
filename_lower.ends_with(".js") ||
filename_lower.ends_with(".properties");
}
} }

View File

@ -1,6 +1,8 @@
#pragma once #pragma once
#include <dirent.h> #include <dirent.h>
#include <string>
#include <vector>
namespace tt::app::files { namespace tt::app::files {
@ -26,19 +28,13 @@ enum {
#define TT_DT_WHT TT_DT_WHT // Whiteout inodes #define TT_DT_WHT TT_DT_WHT // Whiteout inodes
}; };
std::string getChildPath(const std::string& basePath, const std::string& childPath);
typedef int (*ScandirFilter)(const struct dirent*); typedef int (*ScandirFilter)(const struct dirent*);
typedef int (*ScandirSort)(const struct dirent**, const struct dirent**); typedef bool (*ScandirSort)(const struct dirent&, const struct dirent&);
/** bool dirent_sort_alpha_and_type(const struct dirent& left, const struct dirent& right);
* Alphabetic sorting function for tt_scandir()
* @param left left-hand side part for comparison
* @param right right-hand side part for comparison
* @return 0, -1 or 1
*/
int dirent_sort_alpha(const struct dirent** left, const struct dirent** right);
int dirent_sort_alpha_and_type(const struct dirent** left, const struct dirent** right);
int dirent_filter_dot_entries(const struct dirent* entry); int dirent_filter_dot_entries(const struct dirent* entry);
@ -49,16 +45,20 @@ int dirent_filter_dot_entries(const struct dirent* entry);
* The caller is responsible for free-ing the memory of these. * The caller is responsible for free-ing the memory of these.
* *
* @param[in] path path the scan for files and directories * @param[in] path path the scan for files and directories
* @param[out] output a pointer to an array of dirent* * @param[out] outList a pointer to vector of dirent
* @param[in] filter an optional filter to filter out specific items * @param[in] filter an optional filter to filter out specific items
* @param[in] sort an optional sorting function * @param[in] sort an optional sorting function
* @return the amount of items that were stored in "output" or -1 when an error occurred * @return the amount of items that were stored in "output" or -1 when an error occurred
*/ */
int scandir( int scandir(
const char* path, const std::string& path,
struct dirent*** output, std::vector<dirent>& outList,
ScandirFilter _Nullable filter, ScandirFilter _Nullable filter,
ScandirSort _Nullable sort ScandirSort _Nullable sort
); );
bool isSupportedExecutableFile(const std::string& filename);
bool isSupportedImageFile(const std::string& filename);
bool isSupportedTextFile(const std::string& filename);
} // namespace } // namespace

View File

@ -1,21 +1,8 @@
#include "FilesData.h" #include "Files.h"
#include "app/AppContext.h" #include "app/AppContext.h"
#include "Tactility.h"
#include "Assets.h" #include "Assets.h"
#include "Check.h"
#include "FileUtils.h"
#include "StringUtils.h"
#include "app/ElfApp.h"
#include "app/imageviewer/ImageViewer.h"
#include "app/textviewer/TextViewer.h"
#include "lvgl.h"
#include "service/loader/Loader.h"
#include "lvgl/Toolbar.h"
#include <dirent.h>
#include <unistd.h>
#include <memory> #include <memory>
#include <cstring>
namespace tt::app::files { namespace tt::app::files {
@ -24,231 +11,21 @@ namespace tt::app::files {
extern const AppManifest manifest; extern const AppManifest manifest;
/** Returns the app data if the app is active. Note that this could clash if the same app is started twice and a background thread is slow. */ /** Returns the app data if the app is active. Note that this could clash if the same app is started twice and a background thread is slow. */
std::shared_ptr<Data> _Nullable optData() {
app::AppContext* app = service::loader::getCurrentApp();
if (app->getManifest().id == manifest.id) {
return std::static_pointer_cast<Data>(app->getData());
} else {
return nullptr;
}
}
/**
* Case-insensitive check to see if the given file matches the provided file extension.
* @param path the full path to the file
* @param extension the extension to look for, including the period symbol, in lower case
* @return true on match
*/
static bool hasFileExtension(const char* path, const char* extension) {
size_t postfix_len = strlen(extension);
size_t base_len = strlen(path);
if (base_len < postfix_len) {
return false;
}
for (int i = (int)postfix_len - 1; i >= 0; i--) {
if (tolower(path[base_len - postfix_len + i]) != tolower(extension[i])) {
return false;
}
}
return true;
}
static bool isSupportedExecutableFile(const char* filename) {
#ifdef ESP_PLATFORM
// Currently only the PNG library is built into Tactility
return hasFileExtension(filename, ".elf");
#else
return false;
#endif
}
static bool isSupportedImageFile(const char* filename) {
// Currently only the PNG library is built into Tactility
return hasFileExtension(filename, ".png");
}
static bool isSupportedTextFile(const char* filename) {
return hasFileExtension(filename, ".txt") ||
hasFileExtension(filename, ".ini") ||
hasFileExtension(filename, ".json") ||
hasFileExtension(filename, ".yaml") ||
hasFileExtension(filename, ".yml") ||
hasFileExtension(filename, ".lua") ||
hasFileExtension(filename, ".js") ||
hasFileExtension(filename, ".properties");
}
// region Views
static void updateViews(std::shared_ptr<Data> data);
static void onNavigateUpPressed(TT_UNUSED lv_event_t* event) {
auto files_data = optData();
if (files_data == nullptr) {
return;
}
if (strcmp(files_data->current_path, "/") != 0) {
TT_LOG_I(TAG, "Navigating upwards");
char new_absolute_path[MAX_PATH_LENGTH];
if (string::getPathParent(files_data->current_path, new_absolute_path)) {
data_set_entries_for_path(files_data, new_absolute_path);
}
}
updateViews(files_data);
}
static void viewFile(const char* path, const char* filename) {
size_t path_len = strlen(path);
size_t filename_len = strlen(filename);
char* filepath = static_cast<char*>(malloc(path_len + filename_len + 2));
sprintf(filepath, "%s/%s", path, filename);
// For PC we need to make the path relative to the current work directory,
// because that's how LVGL maps its 'drive letter' to the file system.
char* processed_filepath;
if (kernel::getPlatform() == kernel::PlatformSimulator) {
char cwd[PATH_MAX];
if (getcwd(cwd, sizeof(cwd)) == nullptr) {
TT_LOG_E(TAG, "Failed to get current working directory");
return;
}
if (!strstr(filepath, cwd)) {
TT_LOG_E(TAG, "Can only work with files in working directory %s", cwd);
return;
}
char* substr = filepath + strlen(cwd);
processed_filepath = substr;
} else {
processed_filepath = filepath;
}
TT_LOG_I(TAG, "Clicked %s", filepath);
if (isSupportedExecutableFile(filename)) {
#ifdef ESP_PLATFORM
app::startElfApp(processed_filepath);
#endif
} else if (isSupportedImageFile(filename)) {
auto bundle = std::make_shared<Bundle>();
bundle->putString(IMAGE_VIEWER_FILE_ARGUMENT, processed_filepath);
service::loader::startApp("ImageViewer", false, bundle);
} else if (isSupportedTextFile(filename)) {
auto bundle = std::make_shared<Bundle>();
if (kernel::getPlatform() == kernel::PlatformEsp) {
bundle->putString(TEXT_VIEWER_FILE_ARGUMENT, processed_filepath);
} else {
// Remove forward slash, because we need a relative path
bundle->putString(TEXT_VIEWER_FILE_ARGUMENT, processed_filepath + 1);
}
service::loader::startApp("TextViewer", false, bundle);
} else {
TT_LOG_W(TAG, "opening files of this type is not supported");
}
free(filepath);
}
static void onFilePressed(lv_event_t* event) {
auto files_data = optData();
if (files_data == nullptr) {
return;
}
lv_event_code_t code = lv_event_get_code(event);
if (code == LV_EVENT_CLICKED) {
auto* dir_entry = static_cast<dirent*>(lv_event_get_user_data(event));
TT_LOG_I(TAG, "Pressed %s %d", dir_entry->d_name, dir_entry->d_type);
switch (dir_entry->d_type) {
case TT_DT_DIR:
case TT_DT_CHR:
data_set_entries_for_child_path(files_data, dir_entry->d_name);
updateViews(files_data);
break;
case TT_DT_LNK:
TT_LOG_W(TAG, "opening links is not supported");
break;
case TT_DT_REG:
viewFile(files_data->current_path, dir_entry->d_name);
break;
default:
// Assume it's a file
// TODO: Find a better way to identify a file
viewFile(files_data->current_path, dir_entry->d_name);
break;
}
}
}
static void createFileWidget(lv_obj_t* parent, struct dirent* dir_entry) {
tt_check(parent);
auto* list = (lv_obj_t*)parent;
const char* symbol;
if (dir_entry->d_type == TT_DT_DIR || dir_entry->d_type == TT_DT_CHR) {
symbol = LV_SYMBOL_DIRECTORY;
} else if (isSupportedImageFile(dir_entry->d_name)) {
symbol = LV_SYMBOL_IMAGE;
} else if (dir_entry->d_type == TT_DT_LNK) {
symbol = LV_SYMBOL_LOOP;
} else {
symbol = LV_SYMBOL_FILE;
}
lv_obj_t* button = lv_list_add_button(list, symbol, dir_entry->d_name);
lv_obj_add_event_cb(button, &onFilePressed, LV_EVENT_CLICKED, (void*)dir_entry);
}
static void updateViews(std::shared_ptr<Data> data) {
lv_obj_clean(data->list);
for (int i = 0; i < data->dir_entries_count; ++i) {
TT_LOG_D(TAG, "Entry: %s %d", data->dir_entries[i]->d_name, data->dir_entries[i]->d_type);
createFileWidget(data->list, data->dir_entries[i]);
}
}
// endregion Views
// region Lifecycle
static void onShow(AppContext& app, lv_obj_t* parent) { static void onShow(AppContext& app, lv_obj_t* parent) {
auto data = std::static_pointer_cast<Data>(app.getData()); auto files = std::static_pointer_cast<Files>(app.getData());
files->onShow(parent);
lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN);
lv_obj_t* toolbar = lvgl::toolbar_create(parent, "Files");
lvgl::toolbar_add_button_action(toolbar, LV_SYMBOL_UP, &onNavigateUpPressed, nullptr);
data->list = lv_list_create(parent);
lv_obj_set_width(data->list, LV_PCT(100));
lv_obj_set_flex_grow(data->list, 1);
updateViews(data);
} }
static void onStart(AppContext& app) { static void onStart(AppContext& app) {
auto* test = new uint32_t; auto files = std::make_shared<Files>();
delete test; app.setData(files);
auto data = std::make_shared<Data>();
// PC platform is bound to current work directory because of the LVGL file system mapping
if (kernel::getPlatform() == kernel::PlatformSimulator) {
char cwd[PATH_MAX];
if (getcwd(cwd, sizeof(cwd)) != nullptr) {
data_set_entries_for_path(data, cwd);
} else {
TT_LOG_E(TAG, "Failed to get current work directory files");
data_set_entries_for_path(data, "/");
}
} else {
data_set_entries_for_path(data, "/");
}
app.setData(data);
} }
// endregion Lifecycle static void onResult(AppContext& app, Result result, const Bundle& bundle) {
auto files = std::static_pointer_cast<Files>(app.getData());
files->onResult(result, bundle);
}
extern const AppManifest manifest = { extern const AppManifest manifest = {
.id = "Files", .id = "Files",
@ -257,6 +34,7 @@ extern const AppManifest manifest = {
.type = TypeHidden, .type = TypeHidden,
.onStart = onStart, .onStart = onStart,
.onShow = onShow, .onShow = onShow,
.onResult = onResult
}; };
} // namespace } // namespace

View File

@ -0,0 +1,33 @@
#pragma once
#include "View.h"
#include "State.h"
#include "app/AppManifest.h"
#include <lvgl.h>
#include <dirent.h>
#include <memory>
namespace tt::app::files {
class Files {
std::unique_ptr<View> view;
std::shared_ptr<State> state;
public:
Files() {
state = std::make_shared<State>();
view = std::make_unique<View>(state);
}
void onShow(lv_obj_t* parent) {
view->init(parent);
}
void onResult(Result result, const Bundle& bundle) {
view->onResult(result, bundle);
}
};
} // namespace

View File

@ -1,96 +0,0 @@
#include <cstring>
#include "FilesData.h"
#include "FileUtils.h"
#include "StringUtils.h"
#include "Tactility.h"
namespace tt::app::files {
#define TAG "files_app"
static bool get_child_path(char* base_path, const char* child_path, char* out_path, size_t max_chars) {
size_t current_path_length = strlen(base_path);
size_t added_path_length = strlen(child_path);
size_t total_path_length = current_path_length + added_path_length + 1; // two paths with `/`
if (total_path_length >= max_chars) {
TT_LOG_E(TAG, "Path limit reached (%d chars)", MAX_PATH_LENGTH);
return false;
} else {
// Postfix with "/" when the current path isn't "/"
if (current_path_length != 1) {
sprintf(out_path, "%s/%s", base_path, child_path);
} else {
sprintf(out_path, "/%s", child_path);
}
return true;
}
}
static void data_set_entries(std::shared_ptr<Data> data, struct dirent** entries, int count) {
if (data->dir_entries != nullptr) {
data->freeEntries();
}
data->dir_entries = entries;
data->dir_entries_count = count;
}
bool data_set_entries_for_path(std::shared_ptr<Data> data, const char* path) {
TT_LOG_I(TAG, "Changing path: %s -> %s", data->current_path, path);
/**
* ESP32 does not have a root directory, so we have to create it manually.
* We'll add the NVS Flash partitions and the binding for the sdcard.
*/
#if TT_SCREENSHOT_MODE
bool show_custom_root = true;
#else
bool show_custom_root = (kernel::getPlatform() == kernel::PlatformEsp && strcmp(path, "/") == 0);
#endif
if (show_custom_root) {
int dir_entries_count = 3;
auto** dir_entries = (dirent**)malloc(sizeof(struct dirent*) * 3);
dir_entries[0] = (dirent*)malloc(sizeof(struct dirent));
dir_entries[0]->d_type = TT_DT_DIR;
strcpy(dir_entries[0]->d_name, "assets");
dir_entries[1] = (dirent*)malloc(sizeof(struct dirent));
dir_entries[1]->d_type = TT_DT_DIR;
strcpy(dir_entries[1]->d_name, "config");
dir_entries[2] = (dirent*)malloc(sizeof(struct dirent));
dir_entries[2]->d_type = TT_DT_DIR;
strcpy(dir_entries[2]->d_name, "sdcard");
data_set_entries(data, dir_entries, dir_entries_count);
strcpy(data->current_path, path);
return true;
} else {
struct dirent** entries = nullptr;
int count = tt::app::files::scandir(path, &entries, &dirent_filter_dot_entries, &dirent_sort_alpha_and_type);
if (count >= 0) {
TT_LOG_I(TAG, "%s has %u entries", path, count);
data_set_entries(data, entries, count);
strcpy(data->current_path, path);
return true;
} else {
TT_LOG_E(TAG, "Failed to fetch entries for %s", path);
return false;
}
}
}
bool data_set_entries_for_child_path(std::shared_ptr<Data> data, const char* child_path) {
char new_absolute_path[MAX_PATH_LENGTH + 1];
if (get_child_path(data->current_path, child_path, new_absolute_path, MAX_PATH_LENGTH)) {
TT_LOG_I(TAG, "Navigating from %s to %s", data->current_path, new_absolute_path);
return data_set_entries_for_path(data, new_absolute_path);
} else {
TT_LOG_I(TAG, "Failed to get child path for %s/%s", data->current_path, child_path);
return false;
}
}
} // namespace

View File

@ -1,36 +0,0 @@
#pragma once
#include "lvgl.h"
#include <dirent.h>
#include <memory>
namespace tt::app::files {
#define MAX_PATH_LENGTH 256
struct Data {
char current_path[MAX_PATH_LENGTH] = { 0 };
struct dirent** dir_entries = nullptr;
int dir_entries_count = 0;
lv_obj_t* list = nullptr;
void freeEntries() {
for (int i = 0; i < dir_entries_count; ++i) {
free(dir_entries[i]);
}
free(dir_entries);
dir_entries = nullptr;
dir_entries_count = 0;
}
~Data() {
freeEntries();
}
};
void data_free(std::shared_ptr<Data> data);
void data_free_entries(std::shared_ptr<Data> data);
bool data_set_entries_for_child_path(std::shared_ptr<Data> data, const char* child_path);
bool data_set_entries_for_path(std::shared_ptr<Data> data, const char* path);
} // namespace

View File

@ -0,0 +1,106 @@
#include "State.h"
#include "kernel/Kernel.h"
#include "Log.h"
#include "FileUtils.h"
#include <unistd.h>
#define TAG "files_app"
namespace tt::app::files {
State::State() {
if (kernel::getPlatform() == kernel::PlatformSimulator) {
char cwd[PATH_MAX];
if (getcwd(cwd, sizeof(cwd)) != nullptr) {
setEntriesForPath(cwd);
} else {
TT_LOG_E(TAG, "Failed to get current work directory files");
setEntriesForPath("/");
}
} else {
setEntriesForPath("/");
}
}
std::string State::getSelectedChildPath() const {
return getChildPath(current_path, selected_child_entry);
}
bool State::setEntriesForPath(const std::string& path) {
auto scoped_lock = mutex.scoped();
if (!scoped_lock->lock(100)) {
TT_LOG_E(TAG, LOG_MESSAGE_MUTEX_LOCK_FAILED_FMT, "setEntriesForPath");
return false;
}
TT_LOG_I(TAG, "Changing path: %s -> %s", current_path.c_str(), path.c_str());
/**
* ESP32 does not have a root directory, so we have to create it manually.
* We'll add the NVS Flash partitions and the binding for the sdcard.
*/
#if TT_SCREENSHOT_MODE
bool show_custom_root = true;
#else
bool show_custom_root = (kernel::getPlatform() == kernel::PlatformEsp) && (path == "/");
#endif
if (show_custom_root) {
TT_LOG_I(TAG, "Setting custom root");
dir_entries.clear();
dir_entries.push_back({
.d_ino = 0,
.d_type = TT_DT_DIR,
.d_name = "assets"
});
dir_entries.push_back({
.d_ino = 1,
.d_type = TT_DT_DIR,
.d_name = "config"
});
dir_entries.push_back({
.d_ino = 2,
.d_type = TT_DT_DIR,
.d_name = "sdcard"
});
current_path = path;
selected_child_entry = "";
action = ActionNone;
return true;
} else {
dir_entries.clear();
int count = tt::app::files::scandir(path, dir_entries, &dirent_filter_dot_entries, dirent_sort_alpha_and_type);
if (count >= 0) {
TT_LOG_I(TAG, "%s has %u entries", path.c_str(), count);
current_path = path;
selected_child_entry = "";
action = ActionNone;
return true;
} else {
TT_LOG_E(TAG, "Failed to fetch entries for %s", path.c_str());
return false;
}
}
}
bool State::setEntriesForChildPath(const std::string& child_path) {
auto path = getChildPath(current_path, child_path);
TT_LOG_I(TAG, "Navigating from %s to %s", current_path.c_str(), path.c_str());
return setEntriesForPath(path);
}
bool State::getDirent(uint32_t index, dirent& dirent) {
auto scoped_mutex = mutex.scoped();
if (!scoped_mutex->lock(50 / portTICK_PERIOD_MS)) {
return false;
}
if (index < dir_entries.size()) {
dirent = dir_entries[index];
return true;
} else {
return false;
}
}
}

View File

@ -0,0 +1,74 @@
#pragma once
#include <string>
#include <vector>
#include <dirent.h>
#include "Mutex.h"
namespace tt::app::files {
class State {
public:
enum PendingAction {
ActionNone,
ActionDelete,
ActionRename
};
private:
Mutex mutex = Mutex(Mutex::TypeRecursive);
std::vector<dirent> dir_entries;
std::string current_path;
std::string selected_child_entry;
PendingAction action = ActionNone;
public:
State();
void freeEntries() {
dir_entries.clear();
}
~State() {
freeEntries();
}
bool setEntriesForChildPath(const std::string& child_path);
bool setEntriesForPath(const std::string& path);
const std::vector<dirent>& lockEntries() const {
mutex.lock(TtWaitForever);
return dir_entries;
}
void unlockEntries() {
mutex.unlock();
}
bool getDirent(uint32_t index, dirent& dirent);
void setSelectedChildEntry(const std::string& newFile) {
selected_child_entry = newFile;
action = ActionNone;
}
std::string getSelectedChildEntry() const { return selected_child_entry; }
std::string getCurrentPath() const { return current_path; }
std::string getSelectedChildPath() const;
PendingAction getPendingAction() const {
return action;
}
void setPendingAction(PendingAction newAction) {
action = newAction;
}
};
}

View File

@ -0,0 +1,329 @@
#include <cstring>
#include "app/alertdialog/AlertDialog.h"
#include "app/imageviewer/ImageViewer.h"
#include "app/inputdialog/InputDialog.h"
#include "app/textviewer/TextViewer.h"
#include "app/ElfApp.h"
#include "lvgl/Toolbar.h"
#include "lvgl/LvglSync.h"
#include "service/loader/Loader.h"
#include "FileUtils.h"
#include "Tactility.h"
#include "View.h"
#include "StringUtils.h"
#include <filesystem>
#define TAG "files_app"
namespace tt::app::files {
// region Callbacks
static void dirEntryListScrollBeginCallback(lv_event_t* event) {
auto* view = (View*)lv_event_get_user_data(event);
view->onDirEntryListScrollBegin();
}
static void onDirEntryPressedCallback(lv_event_t* event) {
auto* view = (View*)lv_event_get_user_data(event);
auto* button = lv_event_get_target_obj(event);
auto index = lv_obj_get_index(button);
view->onDirEntryPressed(index);
}
static void onDirEntryLongPressedCallback(lv_event_t* event) {
auto* view = (View*)lv_event_get_user_data(event);
auto* button = lv_event_get_target_obj(event);
auto index = lv_obj_get_index(button);
view->onDirEntryLongPressed(index);
}
static void onRenamePressedCallback(lv_event_t* event) {
auto* view = (View*)lv_event_get_user_data(event);
view->onRenamePressed();
}
static void onDeletePressedCallback(lv_event_t* event) {
auto* view = (View*)lv_event_get_user_data(event);
view->onDeletePressed();
}
static void onNavigateUpPressedCallback(TT_UNUSED lv_event_t* event) {
auto* view = (View*)lv_event_get_user_data(event);
view->onNavigateUpPressed();
}
// endregion
void View::viewFile(const std::string& path, const std::string& filename) {
std::string file_path = path + "/" + filename;
// For PC we need to make the path relative to the current work directory,
// because that's how LVGL maps its 'drive letter' to the file system.
std::string processed_filepath;
if (kernel::getPlatform() == kernel::PlatformSimulator) {
char cwd[PATH_MAX];
if (getcwd(cwd, sizeof(cwd)) == nullptr) {
TT_LOG_E(TAG, "Failed to get current working directory");
return;
}
if (!file_path.starts_with(cwd)) {
TT_LOG_E(TAG, "Can only work with files in working directory %s", cwd);
return;
}
processed_filepath = file_path.substr(strlen(cwd));
} else {
processed_filepath = file_path;
}
TT_LOG_I(TAG, "Clicked %s", file_path.c_str());
if (isSupportedExecutableFile(filename)) {
#ifdef ESP_PLATFORM
app::startElfApp(processed_filepath);
#endif
} else if (isSupportedImageFile(filename)) {
auto bundle = std::make_shared<Bundle>();
bundle->putString(IMAGE_VIEWER_FILE_ARGUMENT, processed_filepath);
service::loader::startApp("ImageViewer", false, bundle);
} else if (isSupportedTextFile(filename)) {
auto bundle = std::make_shared<Bundle>();
if (kernel::getPlatform() == kernel::PlatformEsp) {
bundle->putString(TEXT_VIEWER_FILE_ARGUMENT, processed_filepath);
} else {
// Remove forward slash, because we need a relative path
bundle->putString(TEXT_VIEWER_FILE_ARGUMENT, processed_filepath.substr(1));
}
service::loader::startApp("TextViewer", false, bundle);
} else {
TT_LOG_W(TAG, "opening files of this type is not supported");
}
onNavigate();
}
void View::onDirEntryPressed(uint32_t index) {
dirent dir_entry;
if (state->getDirent(index, dir_entry)) {
TT_LOG_I(TAG, "Pressed %s %d", dir_entry.d_name, dir_entry.d_type);
state->setSelectedChildEntry(dir_entry.d_name);
switch (dir_entry.d_type) {
case TT_DT_DIR:
case TT_DT_CHR:
state->setEntriesForChildPath(dir_entry.d_name);
onNavigate();
update();
break;
case TT_DT_LNK:
TT_LOG_W(TAG, "opening links is not supported");
break;
case TT_DT_REG:
viewFile(state->getCurrentPath(), dir_entry.d_name);
onNavigate();
break;
default:
// Assume it's a file
// TODO: Find a better way to identify a file
viewFile(state->getCurrentPath(), dir_entry.d_name);
onNavigate();
break;
}
}
}
void View::onDirEntryLongPressed(int32_t index) {
dirent dir_entry;
if (state->getDirent(index, dir_entry)) {
TT_LOG_I(TAG, "Pressed %s %d", dir_entry.d_name, dir_entry.d_type);
state->setSelectedChildEntry(dir_entry.d_name);
switch (dir_entry.d_type) {
case TT_DT_DIR:
case TT_DT_CHR:
showActionsForDirectory();
break;
case TT_DT_LNK:
TT_LOG_W(TAG, "opening links is not supported");
break;
case TT_DT_REG:
showActionsForFile();
break;
default:
// Assume it's a file
// TODO: Find a better way to identify a file
showActionsForFile();
break;
}
}
}
void View::createDirEntryWidget(lv_obj_t* parent, struct dirent& dir_entry) {
tt_check(parent);
auto* list = (lv_obj_t*)parent;
const char* symbol;
if (dir_entry.d_type == TT_DT_DIR || dir_entry.d_type == TT_DT_CHR) {
symbol = LV_SYMBOL_DIRECTORY;
} else if (isSupportedImageFile(dir_entry.d_name)) {
symbol = LV_SYMBOL_IMAGE;
} else if (dir_entry.d_type == TT_DT_LNK) {
symbol = LV_SYMBOL_LOOP;
} else {
symbol = LV_SYMBOL_FILE;
}
lv_obj_t* button = lv_list_add_button(list, symbol, dir_entry.d_name);
lv_obj_add_event_cb(button, &onDirEntryPressedCallback, LV_EVENT_SHORT_CLICKED, this);
lv_obj_add_event_cb(button, &onDirEntryLongPressedCallback, LV_EVENT_LONG_PRESSED, this);
}
void View::onNavigateUpPressed() {
if (state->getCurrentPath() != "/") {
TT_LOG_I(TAG, "Navigating upwards");
std::string new_absolute_path;
if (string::getPathParent(state->getCurrentPath(), new_absolute_path)) {
state->setEntriesForPath(new_absolute_path);
}
onNavigate();
update();
}
}
void View::onRenamePressed() {
std::string entry_name = state->getSelectedChildEntry();
TT_LOG_I(TAG, "Pending rename %s", entry_name.c_str());
state->setPendingAction(State::ActionRename);
app::inputdialog::start("Rename", "", entry_name);
}
void View::onDeletePressed() {
std::string file_path = state->getSelectedChildPath();
TT_LOG_I(TAG, "Pending delete %s", file_path.c_str());
state->setPendingAction(State::ActionDelete);
std::string message = "Do you want to delete this?\n" + file_path;
const std::vector<std::string> choices = { "Yes", "No" };
app::alertdialog::start("Are you sure?", message, choices);
}
void View::showActionsForDirectory() {
lv_obj_clean(action_list);
auto* rename_button = lv_list_add_button(action_list, LV_SYMBOL_EDIT, "Rename");
lv_obj_add_event_cb(rename_button, onRenamePressedCallback, LV_EVENT_SHORT_CLICKED, this);
lv_obj_remove_flag(action_list, LV_OBJ_FLAG_HIDDEN);
}
void View::showActionsForFile() {
lv_obj_clean(action_list);
auto* rename_button = lv_list_add_button(action_list, LV_SYMBOL_EDIT, "Rename");
lv_obj_add_event_cb(rename_button, onRenamePressedCallback, LV_EVENT_SHORT_CLICKED, this);
auto* delete_button = lv_list_add_button(action_list, LV_SYMBOL_TRASH, "Delete");
lv_obj_add_event_cb(delete_button, onDeletePressedCallback, LV_EVENT_SHORT_CLICKED, this);
lv_obj_remove_flag(action_list, LV_OBJ_FLAG_HIDDEN);
}
void View::update() {
auto scoped_lockable = lvgl::getLvglSyncLockable()->scoped();
if (scoped_lockable->lock(100 / portTICK_PERIOD_MS)) {
lv_obj_clean(dir_entry_list);
auto entries = state->lockEntries();
for (auto entry : entries) {
TT_LOG_D(TAG, "Entry: %s %d", entry.d_name, entry.d_type);
createDirEntryWidget(dir_entry_list, entry);
}
state->unlockEntries();
if (state->getCurrentPath() == "/") {
lv_obj_add_flag(navigate_up_button, LV_OBJ_FLAG_HIDDEN);
} else {
lv_obj_remove_flag(navigate_up_button, LV_OBJ_FLAG_HIDDEN);
}
} else {
TT_LOG_E(TAG, LOG_MESSAGE_MUTEX_LOCK_FAILED_FMT, "lvgl");
}
}
void View::init(lv_obj_t* parent) {
lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN);
lv_obj_t* toolbar = lvgl::toolbar_create(parent, "Files");
navigate_up_button = lvgl::toolbar_add_button_action(toolbar, LV_SYMBOL_UP, &onNavigateUpPressedCallback, this);
lv_obj_t* wrapper = lv_obj_create(parent);
lv_obj_set_width(wrapper, LV_PCT(100));
lv_obj_set_style_border_width(wrapper, 0, 0);
lv_obj_set_style_pad_all(wrapper, 0, 0);
lv_obj_set_flex_grow(wrapper, 1);
lv_obj_set_flex_flow(wrapper, LV_FLEX_FLOW_ROW);
dir_entry_list = lv_list_create(wrapper);
lv_obj_set_height(dir_entry_list, LV_PCT(100));
lv_obj_set_flex_grow(dir_entry_list, 1);
lv_obj_add_event_cb(dir_entry_list, dirEntryListScrollBeginCallback, LV_EVENT_SCROLL_BEGIN, this);
action_list = lv_list_create(wrapper);
lv_obj_set_height(action_list, LV_PCT(100));
lv_obj_set_flex_grow(action_list, 1);
lv_obj_add_flag(action_list, LV_OBJ_FLAG_HIDDEN);
update();
}
void View::onDirEntryListScrollBegin() {
auto scoped_lockable = lvgl::getLvglSyncLockable()->scoped();
if (scoped_lockable->lock(100 / portTICK_PERIOD_MS)) {
lv_obj_add_flag(action_list, LV_OBJ_FLAG_HIDDEN);
}
}
void View::onNavigate() {
auto scoped_lockable = lvgl::getLvglSyncLockable()->scoped();
if (scoped_lockable->lock(100 / portTICK_PERIOD_MS)) {
lv_obj_add_flag(action_list, LV_OBJ_FLAG_HIDDEN);
}
}
void View::onResult(Result result, const Bundle& bundle) {
if (result != ResultOk) {
return;
}
std::string filepath = state->getSelectedChildPath();
TT_LOG_I(TAG, "Result for %s", filepath.c_str());
switch (state->getPendingAction()) {
case State::ActionDelete: {
if (alertdialog::getResultIndex(bundle) == 0) {
int delete_count = (int)remove(filepath.c_str());
if (delete_count > 0) {
TT_LOG_I(TAG, "Deleted %d items", delete_count);
} else {
TT_LOG_W(TAG, "Failed to delete %s", filepath.c_str());
}
state->setEntriesForPath(state->getCurrentPath());
update();
}
break;
}
case State::ActionRename: {
auto new_name = app::inputdialog::getResult(bundle);
if (!new_name.empty() && new_name != state->getSelectedChildEntry()) {
std::string rename_to = getChildPath(state->getCurrentPath(), new_name);
if (rename(filepath.c_str(), rename_to.c_str())) {
TT_LOG_I(TAG, "Renamed \"%s\" to \"%s\"", filepath.c_str(), rename_to.c_str());
} else {
TT_LOG_E(TAG, "Failed to rename \"%s\" to \"%s\"", filepath.c_str(), rename_to.c_str());
}
state->setEntriesForPath(state->getCurrentPath());
update();
}
break;
}
default:
break;
}
}
}

View File

@ -0,0 +1,40 @@
#pragma once
#include "State.h"
#include "app/AppManifest.h"
#include <lvgl.h>
#include <memory>
namespace tt::app::files {
class View {
std::shared_ptr<State> state;
lv_obj_t* dir_entry_list = nullptr;
lv_obj_t* action_list = nullptr;
lv_obj_t* navigate_up_button = nullptr;
void showActionsForDirectory();
void showActionsForFile();
void viewFile(const std::string&path, const std::string&filename);
void createDirEntryWidget(lv_obj_t* parent, struct dirent& dir_entry);
void onNavigate();
public:
explicit View(const std::shared_ptr<State>& state) : state(state) {}
void init(lv_obj_t* parent);
void update();
void onNavigateUpPressed();
void onDirEntryPressed(uint32_t index);
void onDirEntryLongPressed(int32_t index);
void onRenamePressed();
void onDeletePressed();
void onDirEntryListScrollBegin();
void onResult(Result result, const Bundle& bundle);
};
}

View File

@ -89,7 +89,7 @@ static void updateViews(std::shared_ptr<Data> data) {
tt_check(data->mutex.release() == TtStatusOk); tt_check(data->mutex.release() == TtStatusOk);
} else { } else {
TT_LOG_W(TAG, "updateViews lock"); TT_LOG_W(TAG, LOG_MESSAGE_MUTEX_LOCK_FAILED_FMT, "updateViews");
} }
} }
@ -98,7 +98,7 @@ static void updateViewsSafely(std::shared_ptr<Data> data) {
updateViews(data); updateViews(data);
lvgl::unlock(); lvgl::unlock();
} else { } else {
TT_LOG_W(TAG, "updateViewsSafely lock LVGL"); TT_LOG_W(TAG, LOG_MESSAGE_MUTEX_LOCK_FAILED_FMT, "updateViewsSafely");
} }
} }
@ -110,7 +110,7 @@ void onScanTimerFinished(std::shared_ptr<Data> data) {
} }
tt_check(data->mutex.release() == TtStatusOk); tt_check(data->mutex.release() == TtStatusOk);
} else { } else {
TT_LOG_W(TAG, "onScanTimerFinished lock"); TT_LOG_W(TAG, LOG_MESSAGE_MUTEX_LOCK_FAILED_FMT, "onScanTimerFinished");
} }
} }
@ -134,7 +134,7 @@ static void onShow(AppContext& app, lv_obj_t* parent) {
lv_obj_t* scan_button = lv_button_create(wrapper); lv_obj_t* scan_button = lv_button_create(wrapper);
lv_obj_set_width(scan_button, LV_PCT(48)); lv_obj_set_width(scan_button, LV_PCT(48));
lv_obj_align(scan_button, LV_ALIGN_TOP_LEFT, 0, 1); // Shift 1 pixel to align with selection box lv_obj_align(scan_button, LV_ALIGN_TOP_LEFT, 0, 1); // Shift 1 pixel to align with selection box
lv_obj_add_event_cb(scan_button, &onPressScan, LV_EVENT_CLICKED, nullptr); lv_obj_add_event_cb(scan_button, &onPressScan, LV_EVENT_SHORT_CLICKED, nullptr);
lv_obj_t* scan_button_label = lv_label_create(scan_button); lv_obj_t* scan_button_label = lv_label_create(scan_button);
lv_obj_align(scan_button_label, LV_ALIGN_CENTER, 0, 0); lv_obj_align(scan_button_label, LV_ALIGN_CENTER, 0, 0);
lv_label_set_text(scan_button_label, START_SCAN_TEXT); lv_label_set_text(scan_button_label, START_SCAN_TEXT);

View File

@ -22,7 +22,7 @@ static bool getPort(std::shared_ptr<Data> data, i2c_port_t* port) {
tt_assert(data->mutex.release() == TtStatusOk); tt_assert(data->mutex.release() == TtStatusOk);
return true; return true;
} else { } else {
TT_LOG_W(TAG, "getPort lock"); TT_LOG_W(TAG, LOG_MESSAGE_MUTEX_LOCK_FAILED_FMT, "getPort");
return false; return false;
} }
} }
@ -33,7 +33,7 @@ static bool addAddressToList(std::shared_ptr<Data> data, uint8_t address) {
tt_assert(data->mutex.release() == TtStatusOk); tt_assert(data->mutex.release() == TtStatusOk);
return true; return true;
} else { } else {
TT_LOG_W(TAG, "addAddressToList lock"); TT_LOG_W(TAG, LOG_MESSAGE_MUTEX_LOCK_FAILED_FMT, "addAddressToList");
return false; return false;
} }
} }
@ -58,7 +58,7 @@ static void onScanTimer(TT_UNUSED std::shared_ptr<void> context) {
} }
} }
} else { } else {
TT_LOG_W(TAG, "onScanTimer lock"); TT_LOG_W(TAG, LOG_MESSAGE_MUTEX_LOCK_FAILED_FMT, "onScanTimer");
break; break;
} }
@ -82,7 +82,7 @@ bool hasScanThread(std::shared_ptr<Data> data) {
return has_thread; return has_thread;
} else { } else {
// Unsafe way // Unsafe way
TT_LOG_W(TAG, "hasScanTimer lock"); TT_LOG_W(TAG, LOG_MESSAGE_MUTEX_LOCK_FAILED_FMT, "hasScanTimer");
return data->scanTimer != nullptr; return data->scanTimer != nullptr;
} }
} }
@ -107,7 +107,7 @@ void startScanning(std::shared_ptr<Data> data) {
data->scanTimer->start(10); data->scanTimer->start(10);
tt_check(data->mutex.release() == TtStatusOk); tt_check(data->mutex.release() == TtStatusOk);
} else { } else {
TT_LOG_W(TAG, "startScanning lock"); TT_LOG_W(TAG, LOG_MESSAGE_MUTEX_LOCK_FAILED_FMT, "startScanning");
} }
} }
@ -117,7 +117,7 @@ void stopScanning(std::shared_ptr<Data> data) {
data->scanState = ScanStateStopped; data->scanState = ScanStateStopped;
tt_check(data->mutex.release() == TtStatusOk); tt_check(data->mutex.release() == TtStatusOk);
} else { } else {
TT_LOG_E(TAG, "Acquire mutex failed"); TT_LOG_E(TAG, LOG_MESSAGE_MUTEX_LOCK_FAILED);
} }
} }

View File

@ -0,0 +1,120 @@
#include "InputDialog.h"
#include "lvgl.h"
#include "lvgl/Toolbar.h"
#include "service/loader/Loader.h"
#include "service/gui/Gui.h"
#include <StringUtils.h>
#include <TactilityCore.h>
namespace tt::app::inputdialog {
#define PARAMETER_BUNDLE_KEY_TITLE "title"
#define PARAMETER_BUNDLE_KEY_MESSAGE "message"
#define PARAMETER_BUNDLE_KEY_PREFILLED "prefilled"
#define RESULT_BUNDLE_KEY_RESULT "result"
#define DEFAULT_TITLE "Input"
#define TAG "input_dialog"
extern const AppManifest manifest;
void start(const std::string& title, const std::string& message, const std::string& prefilled) {
auto bundle = std::make_shared<Bundle>();
bundle->putString(PARAMETER_BUNDLE_KEY_TITLE, title);
bundle->putString(PARAMETER_BUNDLE_KEY_MESSAGE, message);
bundle->putString(PARAMETER_BUNDLE_KEY_PREFILLED, prefilled);
service::loader::startApp(manifest.id, false, bundle);
}
std::string getResult(const Bundle& bundle) {
std::string result;
bundle.optString(RESULT_BUNDLE_KEY_RESULT, result);
return result;
}
void setResult(const std::shared_ptr<Bundle>& bundle, const std::string& result) {
bundle->putString(RESULT_BUNDLE_KEY_RESULT, result);
}
static std::string getTitleParameter(const std::shared_ptr<const Bundle>& bundle) {
std::string result;
if (bundle->optString(PARAMETER_BUNDLE_KEY_TITLE, result)) {
return result;
} else {
return DEFAULT_TITLE;
}
}
static void onButtonClicked(lv_event_t* e) {
auto user_data = lv_event_get_user_data(e);
int index = (user_data != 0) ? 0 : 1;
TT_LOG_I(TAG, "Selected item at index %d", index);
tt::app::AppContext* app = service::loader::getCurrentApp();
auto bundle = std::make_shared<Bundle>();
if (index == 0) {
const char* text = lv_textarea_get_text((lv_obj_t*)user_data);
setResult(bundle, text);
app->setResult(app::ResultOk, bundle);
} else {
app->setResult(app::ResultCancelled, bundle);
}
service::loader::stopApp();
}
static void createButton(lv_obj_t* parent, const std::string& text, void* callbackContext) {
lv_obj_t* button = lv_button_create(parent);
lv_obj_t* button_label = lv_label_create(button);
lv_obj_align(button_label, LV_ALIGN_CENTER, 0, 0);
lv_label_set_text(button_label, text.c_str());
lv_obj_add_event_cb(button, &onButtonClicked, LV_EVENT_SHORT_CLICKED, callbackContext);
}
static void onShow(AppContext& app, lv_obj_t* parent) {
auto parameters = app.getParameters();
tt_check(parameters != nullptr, "Parameters missing");
std::string title = getTitleParameter(app.getParameters());
auto* toolbar = lvgl::toolbar_create(parent, title);
lv_obj_align(toolbar, LV_ALIGN_TOP_MID, 0, 0);
auto* message_label = lv_label_create(parent);
lv_obj_align(message_label, LV_ALIGN_CENTER, 0, -20);
lv_obj_set_width(message_label, LV_PCT(80));
std::string message;
if (parameters->optString(PARAMETER_BUNDLE_KEY_MESSAGE, message)) {
lv_label_set_text(message_label, message.c_str());
lv_label_set_long_mode(message_label, LV_LABEL_LONG_WRAP);
}
auto* textarea = lv_textarea_create(parent);
lv_obj_align_to(textarea, message_label, LV_ALIGN_OUT_BOTTOM_MID, 0, 4);
lv_textarea_set_one_line(textarea, true);
std::string prefilled;
if (parameters->optString(PARAMETER_BUNDLE_KEY_PREFILLED, prefilled)) {
lv_textarea_set_text(textarea, prefilled.c_str());
}
service::gui::keyboardAddTextArea(textarea);
auto* button_wrapper = lv_obj_create(parent);
lv_obj_set_flex_flow(button_wrapper, LV_FLEX_FLOW_ROW);
lv_obj_set_size(button_wrapper, LV_PCT(100), LV_SIZE_CONTENT);
lv_obj_set_style_pad_all(button_wrapper, 0, 0);
lv_obj_set_flex_align(button_wrapper, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
lv_obj_set_style_border_width(button_wrapper, 0, 0);
lv_obj_align(button_wrapper, LV_ALIGN_BOTTOM_MID, 0, -4);
createButton(button_wrapper, "OK", textarea);
createButton(button_wrapper, "Cancel", nullptr);
}
extern const AppManifest manifest = {
.id = "InputDialog",
.name = "Input Dialog",
.type = TypeHidden,
.onShow = onShow
};
}

View File

@ -0,0 +1,20 @@
#pragma once
#include <string>
#include <vector>
#include "Bundle.h"
/**
* Start the app by its ID and provide:
* - a title
* - a text
*/
namespace tt::app::inputdialog {
void start(const std::string& title, const std::string& message, const std::string& prefilled = "");
/**
* @return the text that was in the field when OK was pressed, or otherwise empty string
*/
std::string getResult(const Bundle& bundle);
}

View File

@ -1,3 +1,7 @@
#include "TactilityConfig.h"
#if TT_FEATURE_SCREENSHOT_ENABLED
#include "ScreenshotUi.h" #include "ScreenshotUi.h"
#include <memory> #include <memory>
@ -5,17 +9,17 @@ namespace tt::app::screenshot {
static void onShow(AppContext& app, lv_obj_t* parent) { static void onShow(AppContext& app, lv_obj_t* parent) {
auto ui = std::static_pointer_cast<ScreenshotUi>(app.getData()); auto ui = std::static_pointer_cast<ScreenshotUi>(app.getData());
create_ui(app, ui, parent); ui->createWidgets(app, parent);
} }
static void onStart(AppContext& app) { static void onStart(AppContext& app) {
auto ui = std::shared_ptr<ScreenshotUi>(new ScreenshotUi()); auto ui = std::make_shared<ScreenshotUi>();
app.setData(ui); // Ensure data gets deleted when no more in use app.setData(ui); // Ensure data gets deleted when no more in use
} }
extern const AppManifest manifest = { extern const AppManifest manifest = {
.id = "Screenshot", .id = "Screenshot",
.name = "_Screenshot", // So it gets put at the bottom of the desktop and becomes less visible on small screen devices .name = "Screenshot",
.icon = LV_SYMBOL_IMAGE, .icon = LV_SYMBOL_IMAGE,
.type = TypeSystem, .type = TypeSystem,
.onStart = onStart, .onStart = onStart,
@ -23,3 +27,5 @@ extern const AppManifest manifest = {
}; };
} // namespace } // namespace
#endif

View File

@ -1,18 +1,23 @@
#include "TactilityConfig.h"
#if TT_FEATURE_SCREENSHOT_ENABLED
#include "ScreenshotUi.h" #include "ScreenshotUi.h"
#include "TactilityCore.h" #include "TactilityCore.h"
#include "hal/sdcard/Sdcard.h" #include "hal/SdCard.h"
#include "service/gui/Gui.h" #include "service/gui/Gui.h"
#include "service/loader/Loader.h" #include "service/loader/Loader.h"
#include "service/screenshot/Screenshot.h" #include "service/screenshot/Screenshot.h"
#include "lvgl/Toolbar.h" #include "lvgl/Toolbar.h"
#include "TactilityHeadless.h"
#include "lvgl/LvglSync.h"
namespace tt::app::screenshot { namespace tt::app::screenshot {
#define TAG "screenshot_ui" #define TAG "screenshot_ui"
extern AppManifest manifest; extern AppManifest manifest;
static void update_mode(std::shared_ptr<ScreenshotUi> ui);
/** Returns the app data if the app is active. Note that this could clash if the same app is started twice and a background thread is slow. */ /** Returns the app data if the app is active. Note that this could clash if the same app is started twice and a background thread is slow. */
std::shared_ptr<ScreenshotUi> _Nullable optScreenshotUi() { std::shared_ptr<ScreenshotUi> _Nullable optScreenshotUi() {
@ -24,173 +29,228 @@ std::shared_ptr<ScreenshotUi> _Nullable optScreenshotUi() {
} }
} }
static void on_start_pressed(lv_event_t* event) { static void onStartPressedCallback(TT_UNUSED lv_event_t* event) {
auto ui = optScreenshotUi(); auto ui = optScreenshotUi();
if (ui == nullptr) { if (ui != nullptr) {
ui->onStartPressed();
}
}
static void onModeSetCallback(TT_UNUSED lv_event_t* event) {
auto ui = optScreenshotUi();
if (ui != nullptr) {
ui->onModeSet();
}
}
static void onTimerCallback(TT_UNUSED std::shared_ptr<void> context) {
auto screenshot_ui = optScreenshotUi();
if (screenshot_ui != nullptr) {
screenshot_ui->onTimerTick();
}
}
ScreenshotUi::ScreenshotUi() {
updateTimer = std::make_unique<Timer>(Timer::TypePeriodic, onTimerCallback, nullptr);
}
ScreenshotUi::~ScreenshotUi() {
if (updateTimer->isRunning()) {
updateTimer->stop();
}
}
void ScreenshotUi::onTimerTick() {
auto lvgl_lock = lvgl::getLvglSyncLockable()->scoped();
if (lvgl_lock->lock(50 / portTICK_PERIOD_MS)) {
updateScreenshotMode();
}
}
void ScreenshotUi::onModeSet() {
updateScreenshotMode();
}
void ScreenshotUi::onStartPressed() {
auto service = service::screenshot::optScreenshotService();
if (service == nullptr) {
TT_LOG_E(TAG, "Service not found/running");
return; return;
} }
if (service::screenshot::isStarted()) { if (service->isTaskStarted()) {
TT_LOG_I(TAG, "Stop screenshot"); TT_LOG_I(TAG, "Stop screenshot");
service::screenshot::stop(); service->stop();
} else { } else {
uint32_t selected = lv_dropdown_get_selected(ui->mode_dropdown); uint32_t selected = lv_dropdown_get_selected(modeDropdown);
const char* path = lv_textarea_get_text(ui->path_textarea); const char* path = lv_textarea_get_text(pathTextArea);
if (selected == 0) { if (selected == 0) {
TT_LOG_I(TAG, "Start timed screenshots"); TT_LOG_I(TAG, "Start timed screenshots");
const char* delay_text = lv_textarea_get_text(ui->delay_textarea); const char* delay_text = lv_textarea_get_text(delayTextArea);
int delay = atoi(delay_text); int delay = atoi(delay_text);
if (delay > 0) { if (delay > 0) {
service::screenshot::startTimed(path, delay, 1); service->startTimed(path, delay, 1);
} else { } else {
TT_LOG_W(TAG, "Ignored screenshot start because delay was 0"); TT_LOG_W(TAG, "Ignored screenshot start because delay was 0");
} }
} else { } else {
TT_LOG_I(TAG, "Start app screenshots"); TT_LOG_I(TAG, "Start app screenshots");
service::screenshot::startApps(path); service->startApps(path);
} }
} }
update_mode(ui); updateScreenshotMode();
} }
static void update_mode(std::shared_ptr<ScreenshotUi> ui) { void ScreenshotUi::updateScreenshotMode() {
lv_obj_t* label = ui->start_stop_button_label; auto service = service::screenshot::optScreenshotService();
if (service::screenshot::isStarted()) { if (service == nullptr) {
TT_LOG_E(TAG, "Service not found/running");
return;
}
lv_obj_t* label = startStopButtonLabel;
if (service->isTaskStarted()) {
lv_label_set_text(label, "Stop"); lv_label_set_text(label, "Stop");
} else { } else {
lv_label_set_text(label, "Start"); lv_label_set_text(label, "Start");
} }
uint32_t selected = lv_dropdown_get_selected(ui->mode_dropdown); uint32_t selected = lv_dropdown_get_selected(modeDropdown);
if (selected == 0) { // Timer if (selected == 0) { // Timer
lv_obj_remove_flag(ui->timer_wrapper, LV_OBJ_FLAG_HIDDEN); lv_obj_remove_flag(timerWrapper, LV_OBJ_FLAG_HIDDEN);
} else { } else {
lv_obj_add_flag(ui->timer_wrapper, LV_OBJ_FLAG_HIDDEN); lv_obj_add_flag(timerWrapper, LV_OBJ_FLAG_HIDDEN);
} }
} }
static void on_mode_set(lv_event_t* event) {
auto ui = optScreenshotUi();
if (ui != nullptr) {
update_mode(ui);
}
}
static void create_mode_setting_ui(std::shared_ptr<ScreenshotUi> ui, lv_obj_t* parent) { void ScreenshotUi::createModeSettingWidgets(lv_obj_t* parent) {
lv_obj_t* mode_wrapper = lv_obj_create(parent); auto service = service::screenshot::optScreenshotService();
if (service == nullptr) {
TT_LOG_E(TAG, "Service not found/running");
return;
}
auto* mode_wrapper = lv_obj_create(parent);
lv_obj_set_size(mode_wrapper, LV_PCT(100), LV_SIZE_CONTENT); lv_obj_set_size(mode_wrapper, LV_PCT(100), LV_SIZE_CONTENT);
lv_obj_set_style_pad_all(mode_wrapper, 0, 0); lv_obj_set_style_pad_all(mode_wrapper, 0, 0);
lv_obj_set_style_border_width(mode_wrapper, 0, 0); lv_obj_set_style_border_width(mode_wrapper, 0, 0);
lv_obj_t* mode_label = lv_label_create(mode_wrapper); auto* mode_label = lv_label_create(mode_wrapper);
lv_label_set_text(mode_label, "Mode:"); lv_label_set_text(mode_label, "Mode:");
lv_obj_align(mode_label, LV_ALIGN_LEFT_MID, 0, 0); lv_obj_align(mode_label, LV_ALIGN_LEFT_MID, 0, 0);
lv_obj_t* mode_dropdown = lv_dropdown_create(mode_wrapper); modeDropdown = lv_dropdown_create(mode_wrapper);
lv_dropdown_set_options(mode_dropdown, "Timer\nApp start"); lv_dropdown_set_options(modeDropdown, "Timer\nApp start");
lv_obj_align_to(mode_dropdown, mode_label, LV_ALIGN_OUT_RIGHT_MID, 8, 0); lv_obj_align_to(modeDropdown, mode_label, LV_ALIGN_OUT_RIGHT_MID, 8, 0);
lv_obj_add_event_cb(mode_dropdown, on_mode_set, LV_EVENT_VALUE_CHANGED, nullptr); lv_obj_add_event_cb(modeDropdown, onModeSetCallback, LV_EVENT_VALUE_CHANGED, nullptr);
ui->mode_dropdown = mode_dropdown; service::screenshot::Mode mode = service->getMode();
service::screenshot::Mode mode = service::screenshot::getMode();
if (mode == service::screenshot::ScreenshotModeApps) { if (mode == service::screenshot::ScreenshotModeApps) {
lv_dropdown_set_selected(mode_dropdown, 1); lv_dropdown_set_selected(modeDropdown, 1);
} }
lv_obj_t* button = lv_button_create(mode_wrapper); auto* button = lv_button_create(mode_wrapper);
lv_obj_align(button, LV_ALIGN_RIGHT_MID, 0, 0); lv_obj_align(button, LV_ALIGN_RIGHT_MID, 0, 0);
lv_obj_t* button_label = lv_label_create(button); lv_obj_add_event_cb(button, &onStartPressedCallback, LV_EVENT_SHORT_CLICKED, nullptr);
lv_obj_align(button_label, LV_ALIGN_CENTER, 0, 0); startStopButtonLabel = lv_label_create(button);
ui->start_stop_button_label = button_label; lv_obj_align(startStopButtonLabel, LV_ALIGN_CENTER, 0, 0);
lv_obj_add_event_cb(button, &on_start_pressed, LV_EVENT_CLICKED, nullptr);
} }
static void create_path_ui(std::shared_ptr<ScreenshotUi> ui, lv_obj_t* parent) { void ScreenshotUi::createFilePathWidgets(lv_obj_t* parent) {
lv_obj_t* path_wrapper = lv_obj_create(parent); auto* path_wrapper = lv_obj_create(parent);
lv_obj_set_size(path_wrapper, LV_PCT(100), LV_SIZE_CONTENT); lv_obj_set_size(path_wrapper, LV_PCT(100), LV_SIZE_CONTENT);
lv_obj_set_style_pad_all(path_wrapper, 0, 0); lv_obj_set_style_pad_all(path_wrapper, 0, 0);
lv_obj_set_style_border_width(path_wrapper, 0, 0); lv_obj_set_style_border_width(path_wrapper, 0, 0);
lv_obj_set_flex_flow(path_wrapper, LV_FLEX_FLOW_ROW); lv_obj_set_flex_flow(path_wrapper, LV_FLEX_FLOW_ROW);
lv_obj_t* label_wrapper = lv_obj_create(path_wrapper); auto* label_wrapper = lv_obj_create(path_wrapper);
lv_obj_set_style_border_width(label_wrapper, 0, 0); lv_obj_set_style_border_width(label_wrapper, 0, 0);
lv_obj_set_style_pad_all(label_wrapper, 0, 0); lv_obj_set_style_pad_all(label_wrapper, 0, 0);
lv_obj_set_size(label_wrapper, 44, 36); lv_obj_set_size(label_wrapper, 44, 36);
lv_obj_t* path_label = lv_label_create(label_wrapper); auto* path_label = lv_label_create(label_wrapper);
lv_label_set_text(path_label, "Path:"); lv_label_set_text(path_label, "Path:");
lv_obj_align(path_label, LV_ALIGN_LEFT_MID, 0, 0); lv_obj_align(path_label, LV_ALIGN_LEFT_MID, 0, 0);
lv_obj_t* path_textarea = lv_textarea_create(path_wrapper); pathTextArea = lv_textarea_create(path_wrapper);
lv_textarea_set_one_line(path_textarea, true); lv_textarea_set_one_line(pathTextArea, true);
lv_obj_set_flex_grow(path_textarea, 1); lv_obj_set_flex_grow(pathTextArea, 1);
ui->path_textarea = path_textarea;
if (kernel::getPlatform() == kernel::PlatformEsp) { if (kernel::getPlatform() == kernel::PlatformEsp) {
if (hal::sdcard::getState() == hal::sdcard::StateMounted) { auto sdcard = tt::hal::getConfiguration().sdcard;
lv_textarea_set_text(path_textarea, "A:/sdcard"); if (sdcard != nullptr && sdcard->getState() == hal::SdCard::StateMounted) {
lv_textarea_set_text(pathTextArea, "A:/sdcard");
} else { } else {
lv_textarea_set_text(path_textarea, "Error: no SD card"); lv_textarea_set_text(pathTextArea, "Error: no SD card");
} }
} else { // PC } else { // PC
lv_textarea_set_text(path_textarea, "A:"); lv_textarea_set_text(pathTextArea, "A:");
} }
} }
static void create_timer_settings_ui(std::shared_ptr<ScreenshotUi> ui, lv_obj_t* parent) { void ScreenshotUi::createTimerSettingsWidgets(lv_obj_t* parent) {
lv_obj_t* timer_wrapper = lv_obj_create(parent); timerWrapper = lv_obj_create(parent);
lv_obj_set_size(timer_wrapper, LV_PCT(100), LV_SIZE_CONTENT); lv_obj_set_size(timerWrapper, LV_PCT(100), LV_SIZE_CONTENT);
lv_obj_set_style_pad_all(timer_wrapper, 0, 0); lv_obj_set_style_pad_all(timerWrapper, 0, 0);
lv_obj_set_style_border_width(timer_wrapper, 0, 0); lv_obj_set_style_border_width(timerWrapper, 0, 0);
ui->timer_wrapper = timer_wrapper;
lv_obj_t* delay_wrapper = lv_obj_create(timer_wrapper); auto* delay_wrapper = lv_obj_create(timerWrapper);
lv_obj_set_size(delay_wrapper, LV_PCT(100), LV_SIZE_CONTENT); lv_obj_set_size(delay_wrapper, LV_PCT(100), LV_SIZE_CONTENT);
lv_obj_set_style_pad_all(delay_wrapper, 0, 0); lv_obj_set_style_pad_all(delay_wrapper, 0, 0);
lv_obj_set_style_border_width(delay_wrapper, 0, 0); lv_obj_set_style_border_width(delay_wrapper, 0, 0);
lv_obj_set_flex_flow(delay_wrapper, LV_FLEX_FLOW_ROW); lv_obj_set_flex_flow(delay_wrapper, LV_FLEX_FLOW_ROW);
lv_obj_t* delay_label_wrapper = lv_obj_create(delay_wrapper); auto* delay_label_wrapper = lv_obj_create(delay_wrapper);
lv_obj_set_style_border_width(delay_label_wrapper, 0, 0); lv_obj_set_style_border_width(delay_label_wrapper, 0, 0);
lv_obj_set_style_pad_all(delay_label_wrapper, 0, 0); lv_obj_set_style_pad_all(delay_label_wrapper, 0, 0);
lv_obj_set_size(delay_label_wrapper, 44, 36); lv_obj_set_size(delay_label_wrapper, 44, 36);
lv_obj_t* delay_label = lv_label_create(delay_label_wrapper); auto* delay_label = lv_label_create(delay_label_wrapper);
lv_label_set_text(delay_label, "Delay:"); lv_label_set_text(delay_label, "Delay:");
lv_obj_align(delay_label, LV_ALIGN_LEFT_MID, 0, 0); lv_obj_align(delay_label, LV_ALIGN_LEFT_MID, 0, 0);
lv_obj_t* delay_textarea = lv_textarea_create(delay_wrapper); delayTextArea = lv_textarea_create(delay_wrapper);
lv_textarea_set_one_line(delay_textarea, true); lv_textarea_set_one_line(delayTextArea, true);
lv_textarea_set_accepted_chars(delay_textarea, "0123456789"); lv_textarea_set_accepted_chars(delayTextArea, "0123456789");
lv_textarea_set_text(delay_textarea, "10"); lv_textarea_set_text(delayTextArea, "10");
lv_obj_set_flex_grow(delay_textarea, 1); lv_obj_set_flex_grow(delayTextArea, 1);
ui->delay_textarea = delay_textarea;
lv_obj_t* delay_unit_label_wrapper = lv_obj_create(delay_wrapper); auto* delay_unit_label_wrapper = lv_obj_create(delay_wrapper);
lv_obj_set_style_border_width(delay_unit_label_wrapper, 0, 0); lv_obj_set_style_border_width(delay_unit_label_wrapper, 0, 0);
lv_obj_set_style_pad_all(delay_unit_label_wrapper, 0, 0); lv_obj_set_style_pad_all(delay_unit_label_wrapper, 0, 0);
lv_obj_set_size(delay_unit_label_wrapper, LV_SIZE_CONTENT, 36); lv_obj_set_size(delay_unit_label_wrapper, LV_SIZE_CONTENT, 36);
lv_obj_t* delay_unit_label = lv_label_create(delay_unit_label_wrapper); auto* delay_unit_label = lv_label_create(delay_unit_label_wrapper);
lv_obj_align(delay_unit_label, LV_ALIGN_LEFT_MID, 0, 0); lv_obj_align(delay_unit_label, LV_ALIGN_LEFT_MID, 0, 0);
lv_label_set_text(delay_unit_label, "seconds"); lv_label_set_text(delay_unit_label, "seconds");
} }
void create_ui(const AppContext& app, std::shared_ptr<ScreenshotUi> ui, lv_obj_t* parent) { void ScreenshotUi::createWidgets(const AppContext& app, lv_obj_t* parent) {
if (updateTimer->isRunning()) {
updateTimer->stop();
}
lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN); lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN);
lv_obj_t* toolbar = lvgl::toolbar_create(parent, app); auto* toolbar = lvgl::toolbar_create(parent, app);
lv_obj_align(toolbar, LV_ALIGN_TOP_MID, 0, 0); lv_obj_align(toolbar, LV_ALIGN_TOP_MID, 0, 0);
lv_obj_t* wrapper = lv_obj_create(parent); auto* wrapper = lv_obj_create(parent);
lv_obj_set_width(wrapper, LV_PCT(100)); lv_obj_set_width(wrapper, LV_PCT(100));
lv_obj_set_flex_grow(wrapper, 1); lv_obj_set_flex_grow(wrapper, 1);
lv_obj_set_style_border_width(wrapper, 0, 0); lv_obj_set_style_border_width(wrapper, 0, 0);
lv_obj_set_flex_flow(wrapper, LV_FLEX_FLOW_COLUMN); lv_obj_set_flex_flow(wrapper, LV_FLEX_FLOW_COLUMN);
create_mode_setting_ui(ui, wrapper); createModeSettingWidgets(wrapper);
create_path_ui(ui, wrapper); createFilePathWidgets(wrapper);
create_timer_settings_ui(ui, wrapper); createTimerSettingsWidgets(wrapper);
service::gui::keyboardAddTextArea(ui->delay_textarea); service::gui::keyboardAddTextArea(delayTextArea);
service::gui::keyboardAddTextArea(ui->path_textarea); service::gui::keyboardAddTextArea(pathTextArea);
update_mode(ui); updateScreenshotMode();
if (!updateTimer->isRunning()) {
updateTimer->start(500 / portTICK_PERIOD_MS);
}
} }
} // namespace } // namespace
#endif

View File

@ -1,3 +1,8 @@
#include "Timer.h"
#include "TactilityConfig.h"
#if TT_FEATURE_SCREENSHOT_ENABLED
#pragma once #pragma once
#include "app/AppContext.h" #include "app/AppContext.h"
@ -5,14 +10,33 @@
namespace tt::app::screenshot { namespace tt::app::screenshot {
typedef struct { class ScreenshotUi {
lv_obj_t* mode_dropdown;
lv_obj_t* path_textarea; lv_obj_t* modeDropdown = nullptr;
lv_obj_t* start_stop_button_label; lv_obj_t* pathTextArea = nullptr;
lv_obj_t* timer_wrapper; lv_obj_t* startStopButtonLabel = nullptr;
lv_obj_t* delay_textarea; lv_obj_t* timerWrapper = nullptr;
} ScreenshotUi; lv_obj_t* delayTextArea = nullptr;
std::unique_ptr<Timer> updateTimer;
void createTimerSettingsWidgets(lv_obj_t* parent);
void createModeSettingWidgets(lv_obj_t* parent);
void createFilePathWidgets(lv_obj_t* parent);
void updateScreenshotMode();
public:
ScreenshotUi();
~ScreenshotUi();
void createWidgets(const AppContext& app, lv_obj_t* parent);
void onStartPressed();
void onModeSet();
void onTimerTick();
};
void create_ui(const AppContext& app, std::shared_ptr<ScreenshotUi> ui, lv_obj_t* parent);
} // namespace } // namespace
#endif

View File

@ -46,8 +46,6 @@ static std::string getTitleParameter(std::shared_ptr<const Bundle> bundle) {
} }
static void onListItemSelected(lv_event_t* e) { static void onListItemSelected(lv_event_t* e) {
lv_event_code_t code = lv_event_get_code(e);
if (code == LV_EVENT_CLICKED) {
size_t index = reinterpret_cast<std::size_t>(lv_event_get_user_data(e)); size_t index = reinterpret_cast<std::size_t>(lv_event_get_user_data(e));
TT_LOG_I(TAG, "Selected item at index %d", index); TT_LOG_I(TAG, "Selected item at index %d", index);
tt::app::AppContext* app = service::loader::getCurrentApp(); tt::app::AppContext* app = service::loader::getCurrentApp();
@ -55,13 +53,12 @@ static void onListItemSelected(lv_event_t* e) {
setResultIndex(bundle, (int32_t)index); setResultIndex(bundle, (int32_t)index);
app->setResult(app::ResultOk, bundle); app->setResult(app::ResultOk, bundle);
service::loader::stopApp(); service::loader::stopApp();
}
} }
static void createChoiceItem(void* parent, const std::string& title, size_t index) { static void createChoiceItem(void* parent, const std::string& title, size_t index) {
auto* list = static_cast<lv_obj_t*>(parent); auto* list = static_cast<lv_obj_t*>(parent);
lv_obj_t* btn = lv_list_add_button(list, nullptr, title.c_str()); lv_obj_t* btn = lv_list_add_button(list, nullptr, title.c_str());
lv_obj_add_event_cb(btn, &onListItemSelected, LV_EVENT_CLICKED, (void*)index); lv_obj_add_event_cb(btn, &onListItemSelected, LV_EVENT_SHORT_CLICKED, (void*)index);
} }
static void onShow(AppContext& app, lv_obj_t* parent) { static void onShow(AppContext& app, lv_obj_t* parent) {

View File

@ -9,11 +9,8 @@
namespace tt::app::settings { namespace tt::app::settings {
static void onAppPressed(lv_event_t* e) { static void onAppPressed(lv_event_t* e) {
lv_event_code_t code = lv_event_get_code(e);
if (code == LV_EVENT_CLICKED) {
const auto* manifest = static_cast<const AppManifest*>(lv_event_get_user_data(e)); const auto* manifest = static_cast<const AppManifest*>(lv_event_get_user_data(e));
service::loader::startApp(manifest->id); service::loader::startApp(manifest->id);
}
} }
static void createWidget(const AppManifest* manifest, void* parent) { static void createWidget(const AppManifest* manifest, void* parent) {
@ -21,7 +18,7 @@ static void createWidget(const AppManifest* manifest, void* parent) {
auto* list = (lv_obj_t*)parent; auto* list = (lv_obj_t*)parent;
const void* icon = !manifest->icon.empty() ? manifest->icon.c_str() : TT_ASSETS_APP_ICON_FALLBACK; const void* icon = !manifest->icon.empty() ? manifest->icon.c_str() : TT_ASSETS_APP_ICON_FALLBACK;
lv_obj_t* btn = lv_list_add_button(list, icon, manifest->name.c_str()); lv_obj_t* btn = lv_list_add_button(list, icon, manifest->name.c_str());
lv_obj_add_event_cb(btn, &onAppPressed, LV_EVENT_CLICKED, (void*)manifest); lv_obj_add_event_cb(btn, &onAppPressed, LV_EVENT_SHORT_CLICKED, (void*)manifest);
} }
static void onShow(AppContext& app, lv_obj_t* parent) { static void onShow(AppContext& app, lv_obj_t* parent) {

View File

@ -100,7 +100,7 @@ static void onShow(AppContext& app, lv_obj_t* parent) {
lv_obj_t* forget_button = lv_button_create(wrapper); lv_obj_t* forget_button = lv_button_create(wrapper);
lv_obj_set_width(forget_button, LV_PCT(100)); lv_obj_set_width(forget_button, LV_PCT(100));
lv_obj_align_to(forget_button, auto_connect_wrapper, LV_ALIGN_OUT_BOTTOM_MID, 0, 10); lv_obj_align_to(forget_button, auto_connect_wrapper, LV_ALIGN_OUT_BOTTOM_MID, 0, 10);
lv_obj_add_event_cb(forget_button, onPressForget, LV_EVENT_CLICKED, nullptr); lv_obj_add_event_cb(forget_button, onPressForget, LV_EVENT_SHORT_CLICKED, nullptr);
lv_obj_t* forget_button_label = lv_label_create(forget_button); lv_obj_t* forget_button_label = lv_label_create(forget_button);
lv_obj_align(forget_button_label, LV_ALIGN_CENTER, 0, 0); lv_obj_align(forget_button_label, LV_ALIGN_CENTER, 0, 0);
lv_label_set_text(forget_button_label, "Forget"); lv_label_set_text(forget_button_label, "Forget");
@ -112,9 +112,10 @@ static void onShow(AppContext& app, lv_obj_t* parent) {
} else { } else {
lv_obj_remove_state(auto_connect_switch, LV_STATE_CHECKED); lv_obj_remove_state(auto_connect_switch, LV_STATE_CHECKED);
} }
} else { } else {
TT_LOG_E(TAG, "Failed to load settings"); TT_LOG_W(TAG, "No settings found");
lv_obj_add_flag(forget_button, LV_OBJ_FLAG_HIDDEN);
lv_obj_add_flag(auto_connect_wrapper, LV_OBJ_FLAG_HIDDEN);
} }
} }

View File

@ -106,7 +106,7 @@ void View::createBottomButtons(lv_obj_t* parent) {
lv_obj_t* connect_label = lv_label_create(connect_button); lv_obj_t* connect_label = lv_label_create(connect_button);
lv_label_set_text(connect_label, "Connect"); lv_label_set_text(connect_label, "Connect");
lv_obj_align(connect_button, LV_ALIGN_RIGHT_MID, 0, 0); lv_obj_align(connect_button, LV_ALIGN_RIGHT_MID, 0, 0);
lv_obj_add_event_cb(connect_button, &onConnect, LV_EVENT_CLICKED, nullptr); lv_obj_add_event_cb(connect_button, &onConnect, LV_EVENT_SHORT_CLICKED, nullptr);
} }
// TODO: Standardize dialogs // TODO: Standardize dialogs

View File

@ -82,7 +82,7 @@ void WifiConnect::requestViewUpdate() {
view.update(); view.update();
lvgl::unlock(); lvgl::unlock();
} else { } else {
TT_LOG_E(TAG, "Failed to lock lvgl"); TT_LOG_E(TAG, LOG_MESSAGE_MUTEX_LOCK_FAILED_FMT, "LVGL");
} }
} }
unlock(); unlock();

View File

@ -76,7 +76,7 @@ static void showDetails(lv_event_t* event) {
void View::createSsidListItem(const service::wifi::WifiApRecord& record, bool isConnecting) { void View::createSsidListItem(const service::wifi::WifiApRecord& record, bool isConnecting) {
lv_obj_t* wrapper = lv_obj_create(networks_list); lv_obj_t* wrapper = lv_obj_create(networks_list);
lv_obj_add_event_cb(wrapper, &connect, LV_EVENT_CLICKED, bindings); lv_obj_add_event_cb(wrapper, &connect, LV_EVENT_SHORT_CLICKED, bindings);
lv_obj_set_user_data(wrapper, bindings); lv_obj_set_user_data(wrapper, bindings);
lv_obj_set_size(wrapper, LV_PCT(100), LV_SIZE_CONTENT); lv_obj_set_size(wrapper, LV_PCT(100), LV_SIZE_CONTENT);
lvgl::obj_set_style_no_padding(wrapper); lvgl::obj_set_style_no_padding(wrapper);
@ -94,7 +94,7 @@ void View::createSsidListItem(const service::wifi::WifiApRecord& record, bool is
lv_obj_set_style_margin_all(info_wrapper, 0, 0); lv_obj_set_style_margin_all(info_wrapper, 0, 0);
lv_obj_set_size(info_wrapper, 36, 36); lv_obj_set_size(info_wrapper, 36, 36);
lv_obj_set_style_border_color(info_wrapper, lv_theme_get_color_primary(info_wrapper), 0); lv_obj_set_style_border_color(info_wrapper, lv_theme_get_color_primary(info_wrapper), 0);
lv_obj_add_event_cb(info_wrapper, &showDetails, LV_EVENT_CLICKED, bindings); lv_obj_add_event_cb(info_wrapper, &showDetails, LV_EVENT_SHORT_CLICKED, bindings);
lv_obj_align(info_wrapper, LV_ALIGN_RIGHT_MID, 0, 0); lv_obj_align(info_wrapper, LV_ALIGN_RIGHT_MID, 0, 0);
lv_obj_t* info_label = lv_label_create(info_wrapper); lv_obj_t* info_label = lv_label_create(info_wrapper);

View File

@ -76,7 +76,7 @@ void WifiManage::requestViewUpdate() {
view.update(); view.update();
lvgl::unlock(); lvgl::unlock();
} else { } else {
TT_LOG_E(TAG, "failed to lock lvgl"); TT_LOG_E(TAG, LOG_MESSAGE_MUTEX_LOCK_FAILED_FMT, "LVGL");
} }
} }
unlock(); unlock();

View File

@ -29,4 +29,24 @@ void unlock() {
unlock_singleton(); unlock_singleton();
} }
class LvglSync : public Lockable {
public:
~LvglSync() override = default;
bool lock(uint32_t timeoutTicks) const override {
return tt::lvgl::lock(timeoutTicks);
}
bool unlock() const override {
tt::lvgl::unlock();
return true;
}
};
static std::shared_ptr<Lockable> lvglSync = std::make_shared<LvglSync>();
std::shared_ptr<Lockable> getLvglSyncLockable() {
return lvglSync;
}
} // namespace } // namespace

View File

@ -1,6 +1,8 @@
#pragma once #pragma once
#include <cstdint> #include "Lockable.h"
#include <memory>
namespace tt::lvgl { namespace tt::lvgl {
@ -12,4 +14,6 @@ bool isSyncSet();
bool lock(uint32_t timeout_ticks); bool lock(uint32_t timeout_ticks);
void unlock(); void unlock();
std::shared_ptr<Lockable> getLvglSyncLockable();
} // namespace } // namespace

View File

@ -1,4 +1,5 @@
#define LV_USE_PRIVATE_API 1 // For actual lv_obj_t declaration #define LV_USE_PRIVATE_API 1 // For actual lv_obj_t declaration
#include "Toolbar.h" #include "Toolbar.h"
#include "service/loader/Loader.h" #include "service/loader/Loader.h"
@ -6,8 +7,6 @@
#include "lvgl/Style.h" #include "lvgl/Style.h"
#include "Spinner.h" #include "Spinner.h"
#define SPINNER_HEIGHT TOOLBAR_HEIGHT
namespace tt::lvgl { namespace tt::lvgl {
typedef struct { typedef struct {
@ -100,20 +99,19 @@ void toolbar_set_title(lv_obj_t* obj, const std::string& title) {
void toolbar_set_nav_action(lv_obj_t* obj, const char* icon, lv_event_cb_t callback, void* user_data) { void toolbar_set_nav_action(lv_obj_t* obj, const char* icon, lv_event_cb_t callback, void* user_data) {
auto* toolbar = (Toolbar*)obj; auto* toolbar = (Toolbar*)obj;
lv_obj_add_event_cb(toolbar->close_button, callback, LV_EVENT_CLICKED, user_data); lv_obj_add_event_cb(toolbar->close_button, callback, LV_EVENT_SHORT_CLICKED, user_data);
lv_image_set_src(toolbar->close_button_image, icon); // e.g. LV_SYMBOL_CLOSE lv_image_set_src(toolbar->close_button_image, icon); // e.g. LV_SYMBOL_CLOSE
} }
lv_obj_t* toolbar_add_button_action(lv_obj_t* obj, const char* icon, lv_event_cb_t callback, void* user_data) { lv_obj_t* toolbar_add_button_action(lv_obj_t* obj, const char* icon, lv_event_cb_t callback, void* user_data) {
auto* toolbar = (Toolbar*)obj; auto* toolbar = (Toolbar*)obj;
uint8_t id = toolbar->action_count;
tt_check(toolbar->action_count < TOOLBAR_ACTION_LIMIT, "max actions reached"); tt_check(toolbar->action_count < TOOLBAR_ACTION_LIMIT, "max actions reached");
toolbar->action_count++; toolbar->action_count++;
lv_obj_t* action_button = lv_button_create(toolbar->action_container); lv_obj_t* action_button = lv_button_create(toolbar->action_container);
lv_obj_set_size(action_button, TOOLBAR_HEIGHT - 4, TOOLBAR_HEIGHT - 4); lv_obj_set_size(action_button, TOOLBAR_HEIGHT - 4, TOOLBAR_HEIGHT - 4);
obj_set_style_no_padding(action_button); obj_set_style_no_padding(action_button);
lv_obj_add_event_cb(action_button, callback, LV_EVENT_CLICKED, user_data); lv_obj_add_event_cb(action_button, callback, LV_EVENT_SHORT_CLICKED, user_data);
lv_obj_t* action_button_image = lv_image_create(action_button); lv_obj_t* action_button_image = lv_image_create(action_button);
lv_image_set_src(action_button_image, icon); lv_image_set_src(action_button_image, icon);
lv_obj_align(action_button_image, LV_ALIGN_CENTER, 0, 0); lv_obj_align(action_button_image, LV_ALIGN_CENTER, 0, 0);
@ -125,6 +123,7 @@ lv_obj_t* toolbar_add_switch_action(lv_obj_t* obj) {
auto* toolbar = (Toolbar*)obj; auto* toolbar = (Toolbar*)obj;
lv_obj_t* widget = lv_switch_create(toolbar->action_container); lv_obj_t* widget = lv_switch_create(toolbar->action_container);
lv_obj_set_style_margin_top(widget, 4, 0); // Because aligning doesn't work lv_obj_set_style_margin_top(widget, 4, 0); // Because aligning doesn't work
lv_obj_set_style_margin_right(widget, 4, 0);
return widget; return widget;
} }

View File

@ -59,7 +59,7 @@ void redraw(Gui* gui) {
// Unlock GUI and LVGL // Unlock GUI and LVGL
lvgl::unlock(); lvgl::unlock();
} else { } else {
TT_LOG_E(TAG, "failed to lock lvgl"); TT_LOG_E(TAG, LOG_MESSAGE_MUTEX_LOCK_FAILED_FMT, "LVGL");
} }
unlock(); unlock();

View File

@ -1,5 +1,3 @@
#include "Tactility.h"
#include <Mutex.h>
#include "app/AppManifest.h" #include "app/AppManifest.h"
#include "app/ManifestRegistry.h" #include "app/ManifestRegistry.h"
#include "service/ServiceManifest.h" #include "service/ServiceManifest.h"
@ -9,95 +7,72 @@
#ifdef ESP_PLATFORM #ifdef ESP_PLATFORM
#include "esp_heap_caps.h" #include "esp_heap_caps.h"
#include "TactilityHeadless.h"
#else #else
#include "lvgl/LvglSync.h" #include "lvgl/LvglSync.h"
#include "TactilityHeadless.h"
#endif #endif
namespace tt::service::loader { namespace tt::service::loader {
#define TAG "loader" #define TAG "loader"
#define LOADER_EVENT_FLAG 1
typedef struct { typedef struct {
LoaderEventType type; LoaderEventType type;
} LoaderEventInternal; } LoaderEventInternal;
// Forward declarations // Forward declarations
static int32_t loader_main(void* p); static void onStartAppMessage(std::shared_ptr<void> message);
static void onStopAppMessage(TT_UNUSED std::shared_ptr<void> message);
static void stopAppInternal();
static LoaderStatus startAppInternal(const std::string& id, std::shared_ptr<const Bundle> _Nullable parameters);
static Loader* loader_singleton = nullptr; static Loader* loader_singleton = nullptr;
static Loader* loader_alloc() { static Loader* loader_alloc() {
assert(loader_singleton == nullptr); assert(loader_singleton == nullptr);
loader_singleton = new Loader(); loader_singleton = new Loader();
loader_singleton->thread = new Thread(
"loader",
4096, // Last known minimum was 2400 for starting Hello World app
&loader_main,
nullptr
);
return loader_singleton; return loader_singleton;
} }
static void loader_free() { static void loader_free() {
tt_assert(loader_singleton != nullptr); tt_assert(loader_singleton != nullptr);
delete loader_singleton->thread;
delete loader_singleton; delete loader_singleton;
loader_singleton = nullptr; loader_singleton = nullptr;
} }
static void loader_lock() { void startApp(const std::string& id, bool blocking, std::shared_ptr<const Bundle> parameters) {
tt_assert(loader_singleton);
tt_check(loader_singleton->mutex.acquire(TtWaitForever) == TtStatusOk);
}
static void loader_unlock() {
tt_assert(loader_singleton);
tt_check(loader_singleton->mutex.release() == TtStatusOk);
}
LoaderStatus startApp(const std::string& id, bool blocking, std::shared_ptr<const Bundle> parameters) {
TT_LOG_I(TAG, "Start app %s", id.c_str()); TT_LOG_I(TAG, "Start app %s", id.c_str());
tt_assert(loader_singleton); tt_assert(loader_singleton);
LoaderMessageLoaderStatusResult result = { auto message = std::make_shared<LoaderMessageAppStart>(id, parameters);
.value = LoaderStatusOk loader_singleton->dispatcherThread->dispatch(onStartAppMessage, message);
};
auto* start_message = new LoaderMessageAppStart(id, parameters); auto event_flag = message->getApiLockEventFlag();
LoaderMessage message(start_message, result); if (blocking) {
EventFlag* event_flag = blocking ? new EventFlag() : nullptr;
if (event_flag != nullptr) {
message.setApiLock(event_flag);
}
loader_singleton->queue.put(&message, TtWaitForever);
if (event_flag != nullptr) {
/* TODO: Check if task id is not the LVGL one, /* TODO: Check if task id is not the LVGL one,
because otherwise this fails as the apps starting logic will try to lock lvgl because otherwise this fails as the apps starting logic will try to lock lvgl
to update the UI and fail. */ to update the UI and fail. */
event_flag->wait(LOADER_EVENT_FLAG); event_flag->wait(message->getApiLockEventFlagValue());
delete event_flag;
} }
return result.value;
} }
void stopApp() { void stopApp() {
TT_LOG_I(TAG, "Stop app"); TT_LOG_I(TAG, "Stop app");
tt_check(loader_singleton); tt_check(loader_singleton);
LoaderMessage message(LoaderMessageTypeAppStop); loader_singleton->dispatcherThread->dispatch(onStopAppMessage, nullptr);
loader_singleton->queue.put(&message, TtWaitForever);
} }
app::AppContext* _Nullable getCurrentApp() { app::AppContext* _Nullable getCurrentApp() {
tt_assert(loader_singleton); tt_assert(loader_singleton);
loader_lock(); if (loader_singleton->mutex.lock(10 / portTICK_PERIOD_MS)) {
app::AppInstance* app = loader_singleton->app_stack.top(); app::AppInstance* app = loader_singleton->app_stack.top();
loader_unlock(); loader_singleton->mutex.unlock();
return dynamic_cast<app::AppContext*>(app); return dynamic_cast<app::AppContext*>(app);
} else {
return nullptr;
}
} }
std::shared_ptr<PubSub> getPubsub() { std::shared_ptr<PubSub> getPubsub() {
@ -108,7 +83,7 @@ std::shared_ptr<PubSub> getPubsub() {
return loader_singleton->pubsub_external; return loader_singleton->pubsub_external;
} }
static const char* app_state_to_string(app::State state) { static const char* appStateToString(app::State state) {
switch (state) { switch (state) {
case app::StateInitial: case app::StateInitial:
return "initial"; return "initial";
@ -125,16 +100,16 @@ static const char* app_state_to_string(app::State state) {
} }
} }
static void app_transition_to_state(app::AppInstance& app, app::State state) { static void transitionAppToState(app::AppInstance& app, app::State state) {
const app::AppManifest& manifest = app.getManifest(); const app::AppManifest& manifest = app.getManifest();
const app::State old_state = app.getState(); const app::State old_state = app.getState();
TT_LOG_I( TT_LOG_I(
TAG, TAG,
"app \"%s\" state: %s -> %s", "App \"%s\" state: %s -> %s",
manifest.id.c_str(), manifest.id.c_str(),
app_state_to_string(old_state), appStateToString(old_state),
app_state_to_string(state) appStateToString(state)
); );
switch (state) { switch (state) {
@ -179,30 +154,33 @@ static void app_transition_to_state(app::AppInstance& app, app::State state) {
} }
} }
static LoaderStatus loader_do_start_app_with_manifest( static LoaderStatus startAppWithManifestInternal(
const app::AppManifest* manifest, const app::AppManifest* manifest,
std::shared_ptr<const Bundle> _Nullable parameters std::shared_ptr<const Bundle> _Nullable parameters
) { ) {
TT_LOG_I(TAG, "start with manifest %s", manifest->id.c_str()); tt_check(loader_singleton != nullptr);
loader_lock(); TT_LOG_I(TAG, "Start with manifest %s", manifest->id.c_str());
auto scoped_lock = loader_singleton->mutex.scoped();
if (!scoped_lock->lock(50 / portTICK_PERIOD_MS)) {
return LoaderStatusErrorInternal;
}
auto previous_app = !loader_singleton->app_stack.empty() ? loader_singleton->app_stack.top() : nullptr; auto previous_app = !loader_singleton->app_stack.empty() ? loader_singleton->app_stack.top() : nullptr;
auto new_app = new app::AppInstance(*manifest, parameters); auto new_app = new app::AppInstance(*manifest, parameters);
new_app->mutableFlags().showStatusbar = (manifest->type != app::TypeBoot); new_app->mutableFlags().showStatusbar = (manifest->type != app::TypeBoot);
loader_singleton->app_stack.push(new_app); loader_singleton->app_stack.push(new_app);
app_transition_to_state(*new_app, app::StateInitial); transitionAppToState(*new_app, app::StateInitial);
app_transition_to_state(*new_app, app::StateStarted); transitionAppToState(*new_app, app::StateStarted);
// We might have to hide the previous app first // We might have to hide the previous app first
if (previous_app != nullptr) { if (previous_app != nullptr) {
app_transition_to_state(*previous_app, app::StateHiding); transitionAppToState(*previous_app, app::StateHiding);
} }
app_transition_to_state(*new_app, app::StateShowing); transitionAppToState(*new_app, app::StateShowing);
loader_unlock();
LoaderEventInternal event_internal = {.type = LoaderEventTypeApplicationStarted}; LoaderEventInternal event_internal = {.type = LoaderEventTypeApplicationStarted};
tt_pubsub_publish(loader_singleton->pubsub_internal, &event_internal); tt_pubsub_publish(loader_singleton->pubsub_internal, &event_internal);
@ -218,7 +196,16 @@ static LoaderStatus loader_do_start_app_with_manifest(
return LoaderStatusOk; return LoaderStatusOk;
} }
static LoaderStatus do_start_by_id( static void onStartAppMessage(std::shared_ptr<void> message) {
auto start_message = std::reinterpret_pointer_cast<LoaderMessageAppStart>(message);
startAppInternal(start_message->id, start_message->parameters);
}
static void onStopAppMessage(TT_UNUSED std::shared_ptr<void> message) {
stopAppInternal();
}
static LoaderStatus startAppInternal(
const std::string& id, const std::string& id,
std::shared_ptr<const Bundle> _Nullable parameters std::shared_ptr<const Bundle> _Nullable parameters
) { ) {
@ -229,18 +216,21 @@ static LoaderStatus do_start_by_id(
TT_LOG_E(TAG, "App not found: %s", id.c_str()); TT_LOG_E(TAG, "App not found: %s", id.c_str());
return LoaderStatusErrorUnknownApp; return LoaderStatusErrorUnknownApp;
} else { } else {
return loader_do_start_app_with_manifest(manifest, parameters); return startAppWithManifestInternal(manifest, parameters);
} }
} }
static void stopAppInternal() {
tt_check(loader_singleton != nullptr);
static void do_stop_app() { auto scoped_lock = loader_singleton->mutex.scoped();
loader_lock(); if (!scoped_lock->lock(50 / portTICK_PERIOD_MS)) {
return;
}
size_t original_stack_size = loader_singleton->app_stack.size(); size_t original_stack_size = loader_singleton->app_stack.size();
if (original_stack_size == 0) { if (original_stack_size == 0) {
loader_unlock();
TT_LOG_E(TAG, "Stop app: no app running"); TT_LOG_E(TAG, "Stop app: no app running");
return; return;
} }
@ -249,7 +239,6 @@ static void do_stop_app() {
app::AppInstance* app_to_stop = loader_singleton->app_stack.top(); app::AppInstance* app_to_stop = loader_singleton->app_stack.top();
if (original_stack_size == 1 && app_to_stop->getManifest().type != app::TypeBoot) { if (original_stack_size == 1 && app_to_stop->getManifest().type != app::TypeBoot) {
loader_unlock();
TT_LOG_E(TAG, "Stop app: can't stop root app"); TT_LOG_E(TAG, "Stop app: can't stop root app");
return; return;
} }
@ -257,8 +246,8 @@ static void do_stop_app() {
std::unique_ptr<app::ResultHolder> result_holder = std::move(app_to_stop->getResult()); std::unique_ptr<app::ResultHolder> result_holder = std::move(app_to_stop->getResult());
const app::AppManifest& manifest = app_to_stop->getManifest(); const app::AppManifest& manifest = app_to_stop->getManifest();
app_transition_to_state(*app_to_stop, app::StateHiding); transitionAppToState(*app_to_stop, app::StateHiding);
app_transition_to_state(*app_to_stop, app::StateStopped); transitionAppToState(*app_to_stop, app::StateStopped);
loader_singleton->app_stack.pop(); loader_singleton->app_stack.pop();
delete app_to_stop; delete app_to_stop;
@ -273,12 +262,14 @@ static void do_stop_app() {
if (!loader_singleton->app_stack.empty()) { if (!loader_singleton->app_stack.empty()) {
app_to_resume = loader_singleton->app_stack.top(); app_to_resume = loader_singleton->app_stack.top();
tt_assert(app_to_resume); tt_assert(app_to_resume);
app_transition_to_state(*app_to_resume, app::StateShowing); transitionAppToState(*app_to_resume, app::StateShowing);
on_result = app_to_resume->getManifest().onResult; on_result = app_to_resume->getManifest().onResult;
} }
loader_unlock(); // Unlock so that we can send results to app and they can also start/stop new apps while processing these results
scoped_lock->unlock();
// WARNING: After this point we cannot change the app states from this method directly anymore as we don't have a lock!
LoaderEventInternal event_internal = {.type = LoaderEventTypeApplicationStopped}; LoaderEventInternal event_internal = {.type = LoaderEventTypeApplicationStopped};
tt_pubsub_publish(loader_singleton->pubsub_internal, &event_internal); tt_pubsub_publish(loader_singleton->pubsub_internal, &event_internal);
@ -319,61 +310,24 @@ static void do_stop_app() {
} }
} }
static int32_t loader_main(TT_UNUSED void* parameter) {
LoaderMessage message;
bool exit_requested = false;
while (!exit_requested) {
tt_assert(loader_singleton != nullptr);
if (loader_singleton->queue.get(&message, TtWaitForever) == TtStatusOk) {
TT_LOG_I(TAG, "Processing message of type %d", message.type);
switch (message.type) {
case LoaderMessageTypeAppStart:
message.result.status_value.value = do_start_by_id(
message.payload.start->id,
message.payload.start->parameters
);
if (message.api_lock != nullptr) {
message.api_lock->set(LOADER_EVENT_FLAG);
}
message.cleanup();
break;
case LoaderMessageTypeAppStop:
do_stop_app();
break;
case LoaderMessageTypeServiceStop:
exit_requested = true;
break;
case LoaderMessageTypeNone:
break;
}
}
}
return 0;
}
// region AppManifest // region AppManifest
static void loader_start(TT_UNUSED ServiceContext& service) { static void loader_start(TT_UNUSED ServiceContext& service) {
tt_check(loader_singleton == nullptr); tt_check(loader_singleton == nullptr);
loader_singleton = loader_alloc(); loader_singleton = loader_alloc();
loader_singleton->dispatcherThread->start();
loader_singleton->thread->setPriority(THREAD_PRIORITY_SERVICE);
loader_singleton->thread->start();
} }
static void loader_stop(TT_UNUSED ServiceContext& service) { static void loader_stop(TT_UNUSED ServiceContext& service) {
tt_check(loader_singleton != nullptr); tt_check(loader_singleton != nullptr);
// Send stop signal to thread and wait for thread to finish // Send stop signal to thread and wait for thread to finish
loader_lock(); if (!loader_singleton->mutex.lock(2000 / portTICK_PERIOD_MS)) {
LoaderMessage message(LoaderMessageTypeServiceStop); TT_LOG_W(TAG, LOG_MESSAGE_MUTEX_LOCK_FAILED_FMT, "loader_stop");
loader_singleton->queue.put(&message, TtWaitForever); }
loader_unlock(); loader_singleton->dispatcherThread->stop();
loader_singleton->thread->join(); loader_singleton->mutex.unlock();
delete loader_singleton->thread;
loader_free(); loader_free();
loader_singleton = nullptr; loader_singleton = nullptr;

View File

@ -23,9 +23,8 @@ typedef enum {
* @param[in] id application name or id * @param[in] id application name or id
* @param[in] blocking whether this call is blocking or not. You cannot call this from an LVGL thread. * @param[in] blocking whether this call is blocking or not. You cannot call this from an LVGL thread.
* @param[in] parameters optional parameters to pass onto the application * @param[in] parameters optional parameters to pass onto the application
* @return LoaderStatus
*/ */
LoaderStatus startApp(const std::string& id, bool blocking = false, std::shared_ptr<const Bundle> _Nullable parameters = nullptr); void startApp(const std::string& id, bool blocking = false, std::shared_ptr<const Bundle> _Nullable parameters = nullptr);
/** /**
* @brief Stop the currently showing app. Show the previous app if any app was still running. * @brief Stop the currently showing app. Show the previous app if any app was still running.

View File

@ -1,9 +1,10 @@
#include "TactilityConfig.h"
#if TT_FEATURE_SCREENSHOT_ENABLED
#include "Screenshot.h" #include "Screenshot.h"
#include <cstdlib>
#include <memory> #include <memory>
#include "Mutex.h"
#include "ScreenshotTask.h"
#include "service/ServiceContext.h" #include "service/ServiceContext.h"
#include "service/ServiceRegistry.h" #include "service/ServiceRegistry.h"
@ -13,105 +14,84 @@ namespace tt::service::screenshot {
extern const ServiceManifest manifest; extern const ServiceManifest manifest;
struct ServiceData { std::shared_ptr<ScreenshotService> _Nullable optScreenshotService() {
Mutex mutex; ServiceContext* context = service::findServiceById(manifest.id);
task::ScreenshotTask* task = nullptr; if (context != nullptr) {
Mode mode = ScreenshotModeNone; return std::static_pointer_cast<ScreenshotService>(context->getData());
} else {
~ServiceData() { return nullptr;
if (task) {
task::free(task);
}
} }
}
void lock() { void ScreenshotService::startApps(const char* path) {
tt_check(mutex.acquire(TtWaitForever) == TtStatusOk); auto scoped_lockable = mutex.scoped();
} if (!scoped_lockable->lock(50 / portTICK_PERIOD_MS)) {
TT_LOG_W(TAG, LOG_MESSAGE_MUTEX_LOCK_FAILED);
void unlock() {
tt_check(mutex.release() == TtStatusOk);
}
};
void startApps(const char* path) {
_Nullable auto* service = findServiceById(manifest.id);
if (service == nullptr) {
TT_LOG_E(TAG, "Service not found");
return; return;
} }
auto data = std::static_pointer_cast<ServiceData>(service->getData()); if (task == nullptr || task->isFinished()) {
data->lock(); task = std::make_unique<ScreenshotTask>();
if (data->task == nullptr) { mode = ScreenshotModeApps;
data->task = task::alloc(); task->startApps(path);
data->mode = ScreenshotModeApps;
task::startApps(data->task, path);
} else { } else {
TT_LOG_E(TAG, "Screenshot task already running"); TT_LOG_W(TAG, "Screenshot task already running");
} }
data->unlock();
} }
void startTimed(const char* path, uint8_t delay_in_seconds, uint8_t amount) { void ScreenshotService::startTimed(const char* path, uint8_t delay_in_seconds, uint8_t amount) {
_Nullable auto* service = findServiceById(manifest.id); auto scoped_lockable = mutex.scoped();
if (service == nullptr) { if (!scoped_lockable->lock(50 / portTICK_PERIOD_MS)) {
TT_LOG_E(TAG, "Service not found"); TT_LOG_W(TAG, LOG_MESSAGE_MUTEX_LOCK_FAILED);
return; return;
} }
auto data = std::static_pointer_cast<ServiceData>(service->getData()); if (task == nullptr || task->isFinished()) {
data->lock(); task = std::make_unique<ScreenshotTask>();
if (data->task == nullptr) { mode = ScreenshotModeTimed;
data->task = task::alloc(); task->startTimed(path, delay_in_seconds, amount);
data->mode = ScreenshotModeTimed;
task::startTimed(data->task, path, delay_in_seconds, amount);
} else { } else {
TT_LOG_E(TAG, "Screenshot task already running"); TT_LOG_W(TAG, "Screenshot task already running");
} }
data->unlock();
} }
void stop() { void ScreenshotService::stop() {
_Nullable ServiceContext* service = findServiceById(manifest.id); auto scoped_lockable = mutex.scoped();
if (service == nullptr) { if (!scoped_lockable->lock(50 / portTICK_PERIOD_MS)) {
TT_LOG_E(TAG, "Service not found"); TT_LOG_W(TAG, LOG_MESSAGE_MUTEX_LOCK_FAILED);
return; return;
} }
auto data = std::static_pointer_cast<ServiceData>(service->getData()); if (task != nullptr) {
data->lock(); task = nullptr;
if (data->task != nullptr) { mode = ScreenshotModeNone;
task::stop(data->task);
task::free(data->task);
data->task = nullptr;
data->mode = ScreenshotModeNone;
} else { } else {
TT_LOG_E(TAG, "Screenshot task not running"); TT_LOG_W(TAG, "Screenshot task not running");
} }
data->unlock();
} }
Mode getMode() { Mode ScreenshotService::getMode() {
_Nullable auto* service = findServiceById(manifest.id); auto scoped_lockable = mutex.scoped();
if (service == nullptr) { if (!scoped_lockable->lock(50 / portTICK_PERIOD_MS)) {
TT_LOG_E(TAG, "Service not found"); TT_LOG_W(TAG, LOG_MESSAGE_MUTEX_LOCK_FAILED);
return ScreenshotModeNone; return ScreenshotModeNone;
} else { }
auto data = std::static_pointer_cast<ServiceData>(service->getData());
data->lock();
Mode mode = data->mode;
data->unlock();
return mode; return mode;
}
bool ScreenshotService::isTaskStarted() {
auto* current_task = task.get();
if (current_task == nullptr) {
return false;
} else {
return !current_task->isFinished();
} }
} }
bool isStarted() { static void onStart(ServiceContext& serviceContext) {
return getMode() != ScreenshotModeNone; auto service = std::make_shared<ScreenshotService>();
} serviceContext.setData(service);
static void onStart(ServiceContext& service) {
auto data = std::make_shared<ServiceData>();
service.setData(data);
} }
extern const ServiceManifest manifest = { extern const ServiceManifest manifest = {
@ -120,3 +100,5 @@ extern const ServiceManifest manifest = {
}; };
} // namespace } // namespace
#endif

View File

@ -1,5 +1,11 @@
#pragma once #pragma once
#include "Mutex.h"
#include "ScreenshotTask.h"
#include "TactilityConfig.h"
#if TT_FEATURE_SCREENSHOT_ENABLED
#include <cstdint> #include <cstdint>
namespace tt::service::screenshot { namespace tt::service::screenshot {
@ -10,22 +16,23 @@ typedef enum {
ScreenshotModeApps ScreenshotModeApps
} Mode; } Mode;
/** @brief Starts taking screenshot with a timer
* @param path the path to store the screenshots in
* @param delay_in_seconds the delay before starting (and between successive screenshots)
* @param amount 0 = indefinite, >0 for a specific
*/
void startTimed(const char* path, uint8_t delay_in_seconds, uint8_t amount);
/** @brief Starts taking screenshot when an app is started class ScreenshotService {
* @param path the path to store the screenshots in Mutex mutex;
*/ std::unique_ptr<ScreenshotTask> task;
void startApps(const char* path); Mode mode = ScreenshotModeNone;
void stop(); public:
Mode getMode(); bool isTaskStarted();
Mode getMode();
void startApps(const char* path);
void startTimed(const char* path, uint8_t delay_in_seconds, uint8_t amount);
void stop();
};
bool isStarted(); std::shared_ptr<ScreenshotService> _Nullable optScreenshotService();
} // namespace } // namespace
#endif

View File

@ -1,184 +1,184 @@
#include "TactilityConfig.h"
#if TT_FEATURE_SCREENSHOT_ENABLED
#include <cstring> #include <cstring>
#include "ScreenshotTask.h" #include "ScreenshotTask.h"
#include "lv_screenshot.h" #include "lv_screenshot.h"
#include "app/AppContext.h" #include "app/AppContext.h"
#include "Mutex.h"
#include "TactilityCore.h" #include "TactilityCore.h"
#include "Thread.h"
#include "service/loader/Loader.h" #include "service/loader/Loader.h"
#include "lvgl/LvglSync.h" #include "lvgl/LvglSync.h"
namespace tt::service::screenshot::task { namespace tt::service::screenshot {
#define TAG "screenshot_task" #define TAG "screenshot_task"
#define TASK_WORK_TYPE_DELAY 1 ScreenshotTask::~ScreenshotTask() {
#define TASK_WORK_TYPE_APPS 2 if (thread) {
stop();
#define SCREENSHOT_PATH_LIMIT 128
struct ScreenshotTaskWork {
int type = TASK_WORK_TYPE_DELAY ;
uint8_t delay_in_seconds = 0;
uint8_t amount = 0;
char path[SCREENSHOT_PATH_LIMIT] = { 0 };
};
struct ScreenshotTaskData {
Thread* thread = nullptr;
Mutex mutex = Mutex(Mutex::TypeRecursive);
bool interrupted = false;
ScreenshotTaskWork work;
};
static void task_lock(ScreenshotTaskData* data) {
tt_check(data->mutex.acquire(TtWaitForever) == TtStatusOk);
}
static void task_unlock(ScreenshotTaskData* data) {
tt_check(data->mutex.release() == TtStatusOk);
}
ScreenshotTask* alloc() {
return new ScreenshotTaskData();
}
void free(ScreenshotTask* task) {
auto* data = static_cast<ScreenshotTaskData*>(task);
if (data->thread) {
stop(data);
} }
delete data;
} }
static bool is_interrupted(ScreenshotTaskData* data) { bool ScreenshotTask::isInterrupted() {
task_lock(data); auto scoped_lockable = mutex.scoped();
bool interrupted = data->interrupted; if (!scoped_lockable->lock(50 / portTICK_PERIOD_MS)) {
task_unlock(data); TT_LOG_W(TAG, LOG_MESSAGE_MUTEX_LOCK_FAILED);
return true;
}
return interrupted; return interrupted;
} }
static int32_t screenshot_task(void* context) { bool ScreenshotTask::isFinished() {
auto* data = static_cast<ScreenshotTaskData*>(context); auto scoped_lockable = mutex.scoped();
if (!scoped_lockable->lock(50 / portTICK_PERIOD_MS)) {
bool interrupted = false; TT_LOG_W(TAG, LOG_MESSAGE_MUTEX_LOCK_FAILED);
uint8_t screenshots_taken = 0; return false;
std::string last_app_id;
while (!interrupted) {
interrupted = is_interrupted(data);
if (data->work.type == TASK_WORK_TYPE_DELAY) {
// Splitting up the delays makes it easier to stop the service
for (int i = 0; i < (data->work.delay_in_seconds * 10) && !is_interrupted(data); ++i){
kernel::delayMillis(100);
} }
return finished;
}
if (is_interrupted(data)) { void ScreenshotTask::setFinished() {
break; auto scoped_lockable = mutex.scoped();
} scoped_lockable->lock(TtWaitForever);
finished = true;
}
screenshots_taken++; static void makeScreenshot(const char* filename) {
char filename[SCREENSHOT_PATH_LIMIT + 32]; if (lvgl::lock(50 / portTICK_PERIOD_MS)) {
sprintf(filename, "%s/screenshot-%d.png", data->work.path, screenshots_taken); if (lv_screenshot_create(lv_scr_act(), LV_100ASK_SCREENSHOT_SV_PNG, filename)) {
lvgl::lock(TtWaitForever);
if (lv_screenshot_create(lv_scr_act(), LV_COLOR_FORMAT_NATIVE, LV_100ASK_SCREENSHOT_SV_PNG, filename)){
TT_LOG_I(TAG, "Screenshot saved to %s", filename); TT_LOG_I(TAG, "Screenshot saved to %s", filename);
} else { } else {
TT_LOG_E(TAG, "Screenshot not saved to %s", filename); TT_LOG_E(TAG, "Screenshot not saved to %s", filename);
} }
lvgl::unlock(); lvgl::unlock();
} else {
TT_LOG_E(TAG, LOG_MESSAGE_MUTEX_LOCK_FAILED_FMT, "LVGL");
}
}
if (data->work.amount > 0 && screenshots_taken >= data->work.amount) { static int32_t screenshotTaskCallback(void* context) {
auto* data = static_cast<ScreenshotTask*>(context);
assert(data != nullptr);
data->taskMain();
return 0;
}
void ScreenshotTask::taskMain() {
uint8_t screenshots_taken = 0;
std::string last_app_id;
while (!isInterrupted()) {
if (work.type == TASK_WORK_TYPE_DELAY) {
// Splitting up the delays makes it easier to stop the service
for (int i = 0; i < (work.delay_in_seconds * 10) && !isInterrupted(); ++i){
kernel::delayMillis(100);
}
if (!isInterrupted()) {
screenshots_taken++;
char filename[SCREENSHOT_PATH_LIMIT + 32];
sprintf(filename, "%s/screenshot-%d.png", work.path, screenshots_taken);
makeScreenshot(filename);
if (work.amount > 0 && screenshots_taken >= work.amount) {
break; // Interrupted loop break; // Interrupted loop
} }
} else if (data->work.type == TASK_WORK_TYPE_APPS) { }
} else if (work.type == TASK_WORK_TYPE_APPS) {
app::AppContext* _Nullable app = loader::getCurrentApp(); app::AppContext* _Nullable app = loader::getCurrentApp();
if (app) { if (app) {
const app::AppManifest& manifest = app->getManifest(); const app::AppManifest& manifest = app->getManifest();
if (manifest.id != last_app_id) { if (manifest.id != last_app_id) {
kernel::delayMillis(100); kernel::delayMillis(100);
last_app_id = manifest.id; last_app_id = manifest.id;
char filename[SCREENSHOT_PATH_LIMIT + 32]; char filename[SCREENSHOT_PATH_LIMIT + 32];
sprintf(filename, "%s/screenshot-%s.png", data->work.path, manifest.id.c_str()); sprintf(filename, "%s/screenshot-%s.png", work.path, manifest.id.c_str());
lvgl::lock(TtWaitForever); makeScreenshot(filename);
if (lv_screenshot_create(lv_scr_act(), LV_COLOR_FORMAT_NATIVE, LV_100ASK_SCREENSHOT_SV_PNG, filename)){
TT_LOG_I(TAG, "Screenshot saved to %s", filename);
} else {
TT_LOG_E(TAG, "Screenshot not saved to %s", filename);
}
lvgl::unlock();
} }
} }
// Ensure the LVGL widgets are rendered as the app just started
kernel::delayMillis(250); kernel::delayMillis(250);
} }
} }
return 0; setFinished();
} }
static void task_start(ScreenshotTaskData* data) { void ScreenshotTask::taskStart() {
task_lock(data); auto scoped_lockable = mutex.scoped();
tt_check(data->thread == nullptr); if (!scoped_lockable->lock(50 / portTICK_PERIOD_MS)) {
data->thread = new Thread( TT_LOG_E(TAG, LOG_MESSAGE_MUTEX_LOCK_FAILED);
return;
}
tt_check(thread == nullptr);
thread = new Thread(
"screenshot", "screenshot",
8192, 8192,
&screenshot_task, &screenshotTaskCallback,
data this
); );
data->thread->start(); thread->start();
task_unlock(data);
} }
void startApps(ScreenshotTask* task, const char* path) { void ScreenshotTask::startApps(const char* path) {
tt_check(strlen(path) < (SCREENSHOT_PATH_LIMIT - 1)); tt_check(strlen(path) < (SCREENSHOT_PATH_LIMIT - 1));
auto* data = static_cast<ScreenshotTaskData*>(task);
task_lock(data); auto scoped_lockable = mutex.scoped();
if (data->thread == nullptr) { if (!scoped_lockable->lock(50 / portTICK_PERIOD_MS)) {
data->interrupted = false; TT_LOG_E(TAG, LOG_MESSAGE_MUTEX_LOCK_FAILED);
data->work.type = TASK_WORK_TYPE_APPS; return;
strcpy(data->work.path, path); }
task_start(data);
if (thread == nullptr) {
interrupted = false;
work.type = TASK_WORK_TYPE_APPS;
strcpy(work.path, path);
taskStart();
} else { } else {
TT_LOG_E(TAG, "Task was already running"); TT_LOG_E(TAG, "Task was already running");
} }
task_unlock(data);
} }
void startTimed(ScreenshotTask* task, const char* path, uint8_t delay_in_seconds, uint8_t amount) { void ScreenshotTask::startTimed(const char* path, uint8_t delay_in_seconds, uint8_t amount) {
tt_check(strlen(path) < (SCREENSHOT_PATH_LIMIT - 1)); tt_check(strlen(path) < (SCREENSHOT_PATH_LIMIT - 1));
auto* data = static_cast<ScreenshotTaskData*>(task); auto scoped_lockable = mutex.scoped();
task_lock(data); if (!scoped_lockable->lock(50 / portTICK_PERIOD_MS)) {
if (data->thread == nullptr) { TT_LOG_E(TAG, LOG_MESSAGE_MUTEX_LOCK_FAILED);
data->interrupted = false; return;
data->work.type = TASK_WORK_TYPE_DELAY; }
data->work.delay_in_seconds = delay_in_seconds;
data->work.amount = amount; if (thread == nullptr) {
strcpy(data->work.path, path); interrupted = false;
task_start(data); work.type = TASK_WORK_TYPE_DELAY;
work.delay_in_seconds = delay_in_seconds;
work.amount = amount;
strcpy(work.path, path);
taskStart();
} else { } else {
TT_LOG_E(TAG, "Task was already running"); TT_LOG_E(TAG, "Task was already running");
} }
task_unlock(data);
} }
void stop(ScreenshotTask* task) { void ScreenshotTask::stop() {
auto* data = static_cast<ScreenshotTaskData*>(task); if (thread != nullptr) {
if (data->thread != nullptr) { if (mutex.lock(50 / portTICK_PERIOD_MS)) {
task_lock(data); interrupted = true;
data->interrupted = true; tt_check(mutex.unlock());
task_unlock(data); }
data->thread->join(); thread->join();
task_lock(data); if (mutex.lock(50 / portTICK_PERIOD_MS)) {
delete data->thread; delete thread;
data->thread = nullptr; thread = nullptr;
task_unlock(data); tt_check(mutex.unlock());
}
} }
} }
} // namespace } // namespace
#endif

View File

@ -1,32 +1,69 @@
#include "TactilityConfig.h"
#if TT_FEATURE_SCREENSHOT_ENABLED
#pragma once #pragma once
#include <cstdint> #include <cstdint>
#include <Thread.h>
#include <Mutex.h>
namespace tt::service::screenshot::task { namespace tt::service::screenshot {
typedef void ScreenshotTask; #define TASK_WORK_TYPE_DELAY 1
#define TASK_WORK_TYPE_APPS 2
ScreenshotTask* alloc(); #define SCREENSHOT_PATH_LIMIT 128
void free(ScreenshotTask* task); class ScreenshotTask {
/** @brief Start taking screenshots after a certain delay struct ScreenshotTaskWork {
int type = TASK_WORK_TYPE_DELAY ;
uint8_t delay_in_seconds = 0;
uint8_t amount = 0;
char path[SCREENSHOT_PATH_LIMIT] = { 0 };
};
Thread* thread = nullptr;
Mutex mutex = Mutex(Mutex::TypeRecursive);
bool interrupted = false;
bool finished = false;
ScreenshotTaskWork work;
public:
ScreenshotTask() = default;
~ScreenshotTask();
/** @brief Start taking screenshots after a certain delay
* @param task the screenshot task * @param task the screenshot task
* @param path the path to store the screenshots at * @param path the path to store the screenshots at
* @param delay_in_seconds the delay before starting (and between successive screenshots) * @param delay_in_seconds the delay before starting (and between successive screenshots)
* @param amount 0 = indefinite, >0 for a specific * @param amount 0 = indefinite, >0 for a specific
*/ */
void startTimed(ScreenshotTask* task, const char* path, uint8_t delay_in_seconds, uint8_t amount); void startTimed(const char* path, uint8_t delay_in_seconds, uint8_t amount);
/** @brief Start taking screenshot whenever an app is started /** @brief Start taking screenshot whenever an app is started
* @param task the screenshot task * @param task the screenshot task
* @param path the path to store the screenshots at * @param path the path to store the screenshots at
*/ */
void startApps(ScreenshotTask* task, const char* path); void startApps(const char* path);
/** @brief Stop taking screenshots /** @brief Stop taking screenshots
* @param task the screenshot task * @param task the screenshot task
*/ */
void stop(ScreenshotTask* task); void stop();
void taskMain();
bool isFinished();
private:
bool isInterrupted();
void setFinished();
void taskStart();
};
} }
#endif

View File

@ -4,11 +4,12 @@
#include "Tactility.h" #include "Tactility.h"
#include "hal/Power.h" #include "hal/Power.h"
#include "hal/sdcard/Sdcard.h" #include "hal/SdCard.h"
#include "lvgl/Statusbar.h" #include "lvgl/Statusbar.h"
#include "service/ServiceContext.h" #include "service/ServiceContext.h"
#include "service/wifi/Wifi.h" #include "service/wifi/Wifi.h"
#include "service/ServiceRegistry.h" #include "service/ServiceRegistry.h"
#include "TactilityHeadless.h"
namespace tt::service::statusbar { namespace tt::service::statusbar {
@ -85,26 +86,30 @@ static void update_wifi_icon(std::shared_ptr<ServiceData> data) {
// region sdcard // region sdcard
static _Nullable const char* sdcard_get_status_icon(hal::sdcard::State state) { static const char* sdcard_get_status_icon(hal::SdCard::State state) {
switch (state) { switch (state) {
case hal::sdcard::StateMounted: case hal::SdCard::StateMounted:
return TT_ASSETS_ICON_SDCARD; return TT_ASSETS_ICON_SDCARD;
case hal::sdcard::StateError: case hal::SdCard::StateError:
case hal::sdcard::StateUnmounted: case hal::SdCard::StateUnmounted:
case hal::SdCard::StateUnknown:
return TT_ASSETS_ICON_SDCARD_ALERT; return TT_ASSETS_ICON_SDCARD_ALERT;
default: default:
return nullptr; tt_crash("Unhandled SdCard state");
} }
} }
static void update_sdcard_icon(std::shared_ptr<ServiceData> data) { static void update_sdcard_icon(std::shared_ptr<ServiceData> data) {
hal::sdcard::State state = hal::sdcard::getState(); auto sdcard = tt::hal::getConfiguration().sdcard;
if (sdcard != nullptr) {
auto state = sdcard->getState();
const char* desired_icon = sdcard_get_status_icon(state); const char* desired_icon = sdcard_get_status_icon(state);
if (data->sdcard_last_icon != desired_icon) { if (data->sdcard_last_icon != desired_icon) {
lvgl::statusbar_icon_set_image(data->sdcard_icon_id, desired_icon); lvgl::statusbar_icon_set_image(data->sdcard_icon_id, desired_icon);
lvgl::statusbar_icon_set_visibility(data->sdcard_icon_id, desired_icon != nullptr); lvgl::statusbar_icon_set_visibility(data->sdcard_icon_id, desired_icon != nullptr);
data->sdcard_last_icon = desired_icon; data->sdcard_last_icon = desired_icon;
} }
}
} }
// endregion sdcard // endregion sdcard

View File

@ -0,0 +1,19 @@
#include "tt_app_alertdialog.h"
#include <app/alertdialog/AlertDialog.h>
extern "C" {
void tt_app_alertdialog_start(const char* title, const char* message, const char* buttonLabels[], uint32_t buttonLabelCount) {
std::vector<std::string> list;
for (int i = 0; i < buttonLabelCount; i++) {
const char* item = buttonLabels[i];
list.push_back(item);
}
tt::app::alertdialog::start(title, message, list);
}
int32_t tt_app_alertdialog_get_result_index(BundleHandle handle) {
return tt::app::alertdialog::getResultIndex(*(tt::Bundle*)handle);
}
}

View File

@ -0,0 +1,16 @@
#pragma once
#include "tt_bundle.h"
#ifdef __cplusplus
extern "C" {
#endif
#include <stdint.h>
void tt_app_alertdialog_start(const char* title, const char* message, const char* buttonLabels[], uint32_t buttonLabelCount);
int32_t tt_app_alertdialog_get_result_index(BundleHandle handle);
#ifdef __cplusplus
}
#endif

View File

@ -0,0 +1,36 @@
#include "tt_app_context.h"
#include <app/AppContext.h>
struct AppContextDataWrapper {
void* _Nullable data;
};
extern "C" {
#define HANDLE_AS_APP_CONTEXT(handle) ((tt::app::AppContext*)(handle))
void* _Nullable tt_app_context_get_data(AppContextHandle handle) {
auto wrapper = std::reinterpret_pointer_cast<AppContextDataWrapper>(HANDLE_AS_APP_CONTEXT(handle)->getData());
return wrapper ? wrapper->data : nullptr;
}
void tt_app_context_set_data(AppContextHandle handle, void* _Nullable data) {
auto wrapper = std::make_shared<AppContextDataWrapper>();
wrapper->data = data;
HANDLE_AS_APP_CONTEXT(handle)->setData(std::move(wrapper));
}
BundleHandle _Nullable tt_app_context_get_parameters(AppContextHandle handle) {
return (BundleHandle)HANDLE_AS_APP_CONTEXT(handle)->getParameters().get();
}
void tt_app_context_set_result(AppContextHandle handle, Result result, BundleHandle _Nullable bundle) {
auto shared_bundle = std::shared_ptr<tt::Bundle>((tt::Bundle*)bundle);
HANDLE_AS_APP_CONTEXT(handle)->setResult((tt::app::Result)result, std::move(shared_bundle));
}
bool tt_app_context_has_result(AppContextHandle handle) {
return HANDLE_AS_APP_CONTEXT(handle)->hasResult();
}
}

View File

@ -0,0 +1,19 @@
#pragma once
#include "tt_app_manifest.h"
#ifdef __cplusplus
extern "C" {
#endif
typedef void* AppContextHandle;
void* _Nullable tt_app_context_get_data(AppContextHandle handle);
void tt_app_context_set_data(AppContextHandle handle, void* _Nullable data);
BundleHandle _Nullable tt_app_context_get_parameters(AppContextHandle handle);
void tt_app_context_set_result(AppContextHandle handle, Result result, BundleHandle _Nullable bundle);
bool tt_app_context_has_result(AppContextHandle handle);
#ifdef __cplusplus
}
#endif

View File

@ -1,9 +1,10 @@
#include <Check.h> #include "tt_app_manifest.h"
#include "App.h"
#include "Log.h"
#include "app/ElfApp.h"
#define TAG "tactilityc_app" #include <Check.h>
#include <Log.h>
#include <app/ElfApp.h>
#define TAG "tt_app"
AppOnStart elfOnStart = nullptr; AppOnStart elfOnStart = nullptr;
AppOnStop elfOnStop = nullptr; AppOnStop elfOnStop = nullptr;
@ -100,7 +101,7 @@ void tt_set_app_manifest(
elfOnResult = onResult; elfOnResult = onResult;
tt::app::setElfAppManifest(manifest); tt::app::setElfAppManifest(manifest);
#else #else
tt_crash("Not intended for PC"); tt_crash("TactilityC is intended for PC/Simulator");
#endif #endif
} }

View File

@ -1,20 +1,20 @@
#pragma once #pragma once
#include "lvgl.h" #include "tt_bundle.h"
#include <lvgl.h>
#ifdef __cplusplus #ifdef __cplusplus
extern "C" { extern "C" {
#endif #endif
typedef void* AppContextHandle;
typedef void* BundleHandle;
typedef enum { typedef enum {
AppResultOk, AppResultOk,
AppResultCancelled, AppResultCancelled,
AppResultError AppResultError
} Result; } Result;
typedef void* AppContextHandle;
typedef void (*AppOnStart)(AppContextHandle app); typedef void (*AppOnStart)(AppContextHandle app);
typedef void (*AppOnStop)(AppContextHandle app); typedef void (*AppOnStop)(AppContextHandle app);
typedef void (*AppOnShow)(AppContextHandle app, lv_obj_t* parent); typedef void (*AppOnShow)(AppContextHandle app, lv_obj_t* parent);

View File

@ -1,4 +1,5 @@
#include "app/selectiondialog/SelectionDialog.h" #include "tt_app_selectiondialog.h"
#include <app/selectiondialog/SelectionDialog.h>
extern "C" { extern "C" {
@ -11,4 +12,8 @@ void tt_app_selectiondialog_start(const char* title, int argc, const char* argv[
tt::app::selectiondialog::start(title, list); tt::app::selectiondialog::start(title, list);
} }
int32_t tt_app_selectiondialog_get_result_index(BundleHandle handle) {
return tt::app::selectiondialog::getResultIndex(*(tt::Bundle*)handle);
}
} }

View File

@ -1,11 +1,15 @@
#pragma once #pragma once
#include "tt_bundle.h"
#ifdef __cplusplus #ifdef __cplusplus
extern "C" { extern "C" {
#endif #endif
void tt_app_selectiondialog_start(const char* title, int argc, const char* argv[]); void tt_app_selectiondialog_start(const char* title, int argc, const char* argv[]);
int32_t tt_app_selectiondialog_get_result_index(BundleHandle handle);
#ifdef __cplusplus #ifdef __cplusplus
} }
#endif #endif

View File

@ -0,0 +1,51 @@
#include <cstring>
#include "tt_bundle.h"
#include "Bundle.h"
#define HANDLE_AS_BUNDLE(handle) ((tt::Bundle*)(handle))
extern "C" {
BundleHandle tt_bundle_alloc() {
return new tt::Bundle();
}
void tt_bundle_free(BundleHandle handle) {
delete HANDLE_AS_BUNDLE(handle);
}
bool tt_bundle_opt_bool(BundleHandle handle, const char* key, bool* out) {
return HANDLE_AS_BUNDLE(handle)->optBool(key, *out);
}
bool tt_bundle_opt_int32(BundleHandle handle, const char* key, int32_t* out) {
return HANDLE_AS_BUNDLE(handle)->optInt32(key, *out);
}
bool tt_bundle_opt_string(BundleHandle handle, const char* key, char* out, uint32_t outSize) {
std::string out_string;
if (HANDLE_AS_BUNDLE(handle)->optString(key, out_string)) {
if (out_string.length() < outSize) { // Need 1 byte to add 0 at the end
memcpy(out, out_string.c_str(), out_string.length());
out[out_string.length()] = 0x00;
return true;
} else {
return false;
}
} else {
return false;
}
}
void tt_bundle_put_bool(BundleHandle handle, const char* key, bool value) {
HANDLE_AS_BUNDLE(handle)->putBool(key, value);
}
void tt_bundle_put_int32(BundleHandle handle, const char* key, int32_t value) {
HANDLE_AS_BUNDLE(handle)->putInt32(key, value);
}
void tt_bundle_put_string(BundleHandle handle, const char* key, const char* value) {
HANDLE_AS_BUNDLE(handle)->putString(key, value);
}
}

View File

@ -0,0 +1,29 @@
#pragma once
#ifdef __cplusplus
extern "C" {
#endif
#include <stdint.h>
#include <stdbool.h>
typedef void* BundleHandle;
BundleHandle tt_bundle_alloc();
void tt_bundle_free(BundleHandle handle);
bool tt_bundle_opt_bool(BundleHandle handle, const char* key, bool* out);
bool tt_bundle_opt_int32(BundleHandle handle, const char* key, int32_t* out);
/**
* Note that outSize must be large enough to include null terminator.
* This means that your string has to be the expected text length + 1 extra character.
*/
bool tt_bundle_opt_string(BundleHandle handle, const char* key, char* out, uint32_t outSize);
void tt_bundle_put_bool(BundleHandle handle, const char* key, bool value);
void tt_bundle_put_int32(BundleHandle handle, const char* key, int32_t value);
void tt_bundle_put_string(BundleHandle handle, const char* key, const char* value);
#ifdef __cplusplus
}
#endif

View File

@ -2,21 +2,86 @@
#include "elf_symbol.h" #include "elf_symbol.h"
#include "app/App.h" #include "tt_app_context.h"
#include "app/SelectionDialog.h" #include "tt_app_manifest.h"
#include "lvgl/Toolbar.h" #include "tt_app_alertdialog.h"
#include "TactilityC/lvgl/Spinner.h" #include "tt_app_selectiondialog.h"
#include "tt_bundle.h"
#include "tt_lvgl_spinner.h"
#include "tt_lvgl_toolbar.h"
#include "tt_message_queue.h"
#include "tt_mutex.h"
#include "tt_semaphore.h"
#include "tt_thread.h"
#include "tt_timer.h"
#include "lvgl.h" #include <lvgl.h>
extern "C" { extern "C" {
const struct esp_elfsym elf_symbols[] { const struct esp_elfsym elf_symbols[] {
// Tactility // Tactility
ESP_ELFSYM_EXPORT(tt_app_context_get_data),
ESP_ELFSYM_EXPORT(tt_app_context_set_data),
ESP_ELFSYM_EXPORT(tt_app_context_get_parameters),
ESP_ELFSYM_EXPORT(tt_app_context_set_result),
ESP_ELFSYM_EXPORT(tt_app_context_has_result),
ESP_ELFSYM_EXPORT(tt_app_selectiondialog_start), ESP_ELFSYM_EXPORT(tt_app_selectiondialog_start),
ESP_ELFSYM_EXPORT(tt_app_selectiondialog_get_result_index),
ESP_ELFSYM_EXPORT(tt_app_alertdialog_start),
ESP_ELFSYM_EXPORT(tt_app_alertdialog_get_result_index),
ESP_ELFSYM_EXPORT(tt_bundle_alloc),
ESP_ELFSYM_EXPORT(tt_bundle_free),
ESP_ELFSYM_EXPORT(tt_bundle_opt_bool),
ESP_ELFSYM_EXPORT(tt_bundle_opt_int32),
ESP_ELFSYM_EXPORT(tt_bundle_opt_string),
ESP_ELFSYM_EXPORT(tt_bundle_put_bool),
ESP_ELFSYM_EXPORT(tt_bundle_put_int32),
ESP_ELFSYM_EXPORT(tt_bundle_put_string),
ESP_ELFSYM_EXPORT(tt_set_app_manifest), ESP_ELFSYM_EXPORT(tt_set_app_manifest),
ESP_ELFSYM_EXPORT(tt_lvgl_toolbar_create), ESP_ELFSYM_EXPORT(tt_lvgl_toolbar_create),
ESP_ELFSYM_EXPORT(tt_lvgl_toolbar_create_simple), ESP_ELFSYM_EXPORT(tt_lvgl_toolbar_create_simple),
ESP_ELFSYM_EXPORT(tt_message_queue_alloc),
ESP_ELFSYM_EXPORT(tt_message_queue_free),
ESP_ELFSYM_EXPORT(tt_message_queue_put),
ESP_ELFSYM_EXPORT(tt_message_queue_get),
ESP_ELFSYM_EXPORT(tt_message_queue_get_capacity),
ESP_ELFSYM_EXPORT(tt_message_queue_get_message_size),
ESP_ELFSYM_EXPORT(tt_message_queue_get_count),
ESP_ELFSYM_EXPORT(tt_message_queue_reset),
ESP_ELFSYM_EXPORT(tt_mutex_alloc),
ESP_ELFSYM_EXPORT(tt_mutex_free),
ESP_ELFSYM_EXPORT(tt_mutex_lock),
ESP_ELFSYM_EXPORT(tt_mutex_unlock),
ESP_ELFSYM_EXPORT(tt_semaphore_alloc),
ESP_ELFSYM_EXPORT(tt_semaphore_free),
ESP_ELFSYM_EXPORT(tt_semaphore_acquire),
ESP_ELFSYM_EXPORT(tt_semaphore_release),
ESP_ELFSYM_EXPORT(tt_semaphore_get_count),
ESP_ELFSYM_EXPORT(tt_thread_alloc),
ESP_ELFSYM_EXPORT(tt_thread_alloc_ext),
ESP_ELFSYM_EXPORT(tt_thread_free),
ESP_ELFSYM_EXPORT(tt_thread_set_name),
ESP_ELFSYM_EXPORT(tt_thread_mark_as_static),
ESP_ELFSYM_EXPORT(tt_thread_is_marked_as_static),
ESP_ELFSYM_EXPORT(tt_thread_set_stack_size),
ESP_ELFSYM_EXPORT(tt_thread_set_callback),
ESP_ELFSYM_EXPORT(tt_thread_set_priority),
ESP_ELFSYM_EXPORT(tt_thread_set_state_callback),
ESP_ELFSYM_EXPORT(tt_thread_get_state),
ESP_ELFSYM_EXPORT(tt_thread_start),
ESP_ELFSYM_EXPORT(tt_thread_join),
ESP_ELFSYM_EXPORT(tt_thread_get_id),
ESP_ELFSYM_EXPORT(tt_thread_get_return_code),
ESP_ELFSYM_EXPORT(tt_timer_alloc),
ESP_ELFSYM_EXPORT(tt_timer_free),
ESP_ELFSYM_EXPORT(tt_timer_start),
ESP_ELFSYM_EXPORT(tt_timer_restart),
ESP_ELFSYM_EXPORT(tt_timer_stop),
ESP_ELFSYM_EXPORT(tt_timer_is_running),
ESP_ELFSYM_EXPORT(tt_timer_get_expire_time),
ESP_ELFSYM_EXPORT(tt_timer_set_pending_callback),
ESP_ELFSYM_EXPORT(tt_timer_set_thread_priority),
// tt::lvgl // tt::lvgl
ESP_ELFSYM_EXPORT(tt_lvgl_spinner_create), ESP_ELFSYM_EXPORT(tt_lvgl_spinner_create),
// lv_obj // lv_obj

View File

@ -1,4 +1,4 @@
#include "Spinner.h" #include "tt_lvgl_spinner.h"
#include "lvgl/Spinner.h" #include "lvgl/Spinner.h"
extern "C" { extern "C" {

View File

@ -1,6 +1,6 @@
#pragma once #pragma once
#include "lvgl.h" #include <lvgl.h>
#ifdef __cplusplus #ifdef __cplusplus
extern "C" { extern "C" {

View File

@ -1,5 +1,5 @@
#include "Toolbar.h" #include "tt_lvgl_toolbar.h"
#include "lvgl/Toolbar.h" #include <lvgl/Toolbar.h>
extern "C" { extern "C" {

View File

@ -1,7 +1,7 @@
#pragma once #pragma once
#include "lvgl.h" #include <lvgl.h>
#include "TactilityC/app/App.h" #include "tt_app_context.h"
#ifdef __cplusplus #ifdef __cplusplus
extern "C" { extern "C" {

View File

@ -0,0 +1,40 @@
#include "tt_message_queue.h"
#include <MessageQueue.h>
#define HANDLE_TO_MESSAGE_QUEUE(handle) ((tt::MessageQueue*)(handle))
extern "C" {
MessageQueueHandle tt_message_queue_alloc(uint32_t capacity, uint32_t messageSize) {
return new tt::MessageQueue(capacity, messageSize);
}
void tt_message_queue_free(MessageQueueHandle handle) {
delete HANDLE_TO_MESSAGE_QUEUE(handle);
}
bool tt_message_queue_put(MessageQueueHandle handle, const void* message, uint32_t timeout) {
return HANDLE_TO_MESSAGE_QUEUE(handle)->put(message, timeout);
}
bool tt_message_queue_get(MessageQueueHandle handle, void* message, uint32_t timeout) {
return HANDLE_TO_MESSAGE_QUEUE(handle)->get(message, timeout);
}
uint32_t tt_message_queue_get_capacity(MessageQueueHandle handle) {
return HANDLE_TO_MESSAGE_QUEUE(handle)->getCapacity();
}
uint32_t tt_message_queue_get_message_size(MessageQueueHandle handle) {
return HANDLE_TO_MESSAGE_QUEUE(handle)->getMessageSize();
}
uint32_t tt_message_queue_get_count(MessageQueueHandle handle) {
return HANDLE_TO_MESSAGE_QUEUE(handle)->getCount();
}
bool tt_message_queue_reset(MessageQueueHandle handle) {
return HANDLE_TO_MESSAGE_QUEUE(handle)->reset();
}
}

View File

@ -0,0 +1,23 @@
#pragma once
#ifdef __cplusplus
extern "C" {
#endif
#include <stdint.h>
#include <stdbool.h>
typedef void* MessageQueueHandle;
MessageQueueHandle tt_message_queue_alloc(uint32_t capacity, uint32_t messageSize);
void tt_message_queue_free(MessageQueueHandle handle);
bool tt_message_queue_put(MessageQueueHandle handle, const void* message, uint32_t timeout);
bool tt_message_queue_get(MessageQueueHandle handle, void* message, uint32_t timeout);
uint32_t tt_message_queue_get_capacity(MessageQueueHandle handle);
uint32_t tt_message_queue_get_message_size(MessageQueueHandle handle);
uint32_t tt_message_queue_get_count(MessageQueueHandle handle);
bool tt_message_queue_reset(MessageQueueHandle handle);
#ifdef __cplusplus
}
#endif

View File

@ -0,0 +1,31 @@
#include "tt_mutex.h"
#include "Mutex.h"
extern "C" {
#define HANDLE_AS_MUTEX(handle) ((tt::Mutex*)(handle))
MutexHandle tt_mutex_alloc(enum TtMutexType type) {
switch (type) {
case TtMutexType::MUTEX_TYPE_NORMAL:
return new tt::Mutex(tt::Mutex::TypeNormal);
case TtMutexType::MUTEX_TYPE_RECURSIVE:
return new tt::Mutex(tt::Mutex::TypeRecursive);
default:
tt_crash("Type not supported");
}
}
void tt_mutex_free(MutexHandle handle) {
delete HANDLE_AS_MUTEX(handle);
}
bool tt_mutex_lock(MutexHandle handle, uint32_t timeoutTicks) {
return HANDLE_AS_MUTEX(handle)->lock(timeoutTicks);
}
bool tt_mutex_unlock(MutexHandle handle) {
return HANDLE_AS_MUTEX(handle)->unlock();
}
}

View File

@ -0,0 +1,24 @@
#pragma once
#ifdef __cplusplus
extern "C" {
#endif
#include <stdint.h>
#include <stdbool.h>
typedef void* MutexHandle;
enum TtMutexType {
MUTEX_TYPE_NORMAL,
MUTEX_TYPE_RECURSIVE
};
MutexHandle tt_mutex_alloc(enum TtMutexType);
void tt_mutex_free(MutexHandle handle);
bool tt_mutex_lock(MutexHandle handle, uint32_t timeoutTicks);
bool tt_mutex_unlock(MutexHandle handle);
#ifdef __cplusplus
}
#endif

View File

@ -0,0 +1,28 @@
#include "tt_semaphore.h"
#include "Semaphore.h"
extern "C" {
#define HANDLE_AS_SEMAPHORE(handle) ((tt::Semaphore*)(handle))
SemaphoreHandle tt_semaphore_alloc(uint32_t maxCount, uint32_t initialCount) {
return new tt::Semaphore(maxCount, initialCount);
}
void tt_semaphore_free(SemaphoreHandle handle) {
delete HANDLE_AS_SEMAPHORE(handle);
}
bool tt_semaphore_acquire(SemaphoreHandle handle, uint32_t timeoutTicks) {
return HANDLE_AS_SEMAPHORE(handle)->acquire(timeoutTicks);
}
bool tt_semaphore_release(SemaphoreHandle handle) {
return HANDLE_AS_SEMAPHORE(handle)->release();
}
uint32_t tt_semaphore_get_count(SemaphoreHandle handle) {
return HANDLE_AS_SEMAPHORE(handle)->getCount();
}
}

View File

@ -0,0 +1,20 @@
#pragma once
#ifdef __cplusplus
extern "C" {
#endif
#include <stdint.h>
#include <stdbool.h>
typedef void* SemaphoreHandle;
SemaphoreHandle tt_semaphore_alloc(uint32_t maxCount, uint32_t initialCount);
void tt_semaphore_free(SemaphoreHandle handle);
bool tt_semaphore_acquire(SemaphoreHandle handle, uint32_t timeoutTicks);
bool tt_semaphore_release(SemaphoreHandle handle);
uint32_t tt_semaphore_get_count(SemaphoreHandle handle);
#ifdef __cplusplus
}
#endif

Some files were not shown because too many files have changed in this diff Show More