mirror of
https://github.com/ByteWelder/Tactility.git
synced 2026-04-23 11:55:05 +00:00
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:
parent
9033daa6dd
commit
50bd6e8bf6
@ -2,7 +2,7 @@
|
||||
|
||||
// Apps
|
||||
#include "Tactility.h"
|
||||
#include "TactilityC/TactilityC.h"
|
||||
#include "tt_init.h"
|
||||
|
||||
namespace tt::service::wifi {
|
||||
extern void wifi_task(void*);
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
idf_component_register(
|
||||
SRC_DIRS "Source" "Source/hal"
|
||||
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
|
||||
)
|
||||
|
||||
@ -19,25 +19,32 @@
|
||||
#define TDECK_LCD_BACKLIGHT_LEDC_FREQUENCY (4000)
|
||||
|
||||
static bool init_spi() {
|
||||
TT_LOG_I(TAG, LOG_MESSAGE_SPI_INIT_START_FMT, TDECK_SPI_HOST);
|
||||
|
||||
spi_bus_config_t bus_config = {
|
||||
.mosi_io_num = TDECK_SPI_PIN_MOSI,
|
||||
.miso_io_num = TDECK_SPI_PIN_MISO,
|
||||
.sclk_io_num = TDECK_SPI_PIN_SCLK,
|
||||
.quadwp_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,
|
||||
.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;
|
||||
}
|
||||
|
||||
bool tdeck_init_hardware() {
|
||||
TT_LOG_I(TAG, "Init SPI");
|
||||
|
||||
if (!init_spi()) {
|
||||
TT_LOG_E(TAG, "Init SPI failed");
|
||||
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);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool tdeck_init_hardware() {
|
||||
return init_spi();
|
||||
}
|
||||
|
||||
@ -2,21 +2,19 @@
|
||||
#include "hal/TdeckDisplay.h"
|
||||
#include "hal/TdeckKeyboard.h"
|
||||
#include "hal/TdeckPower.h"
|
||||
#include "hal/sdcard/Sdcard.h"
|
||||
#include "hal/TdeckSdCard.h"
|
||||
|
||||
bool tdeck_init_power();
|
||||
bool tdeck_init_hardware();
|
||||
bool tdeck_init_lvgl();
|
||||
|
||||
extern const tt::hal::sdcard::SdCard tdeck_sdcard;
|
||||
|
||||
extern const tt::hal::Configuration lilygo_tdeck = {
|
||||
.initBoot = tdeck_init_power,
|
||||
.initHardware = tdeck_init_hardware,
|
||||
.initLvgl = tdeck_init_lvgl,
|
||||
.createDisplay = createDisplay,
|
||||
.createKeyboard = createKeyboard,
|
||||
.sdcard = &tdeck_sdcard,
|
||||
.sdcard = createTdeckSdCard(),
|
||||
.power = tdeck_get_power,
|
||||
.i2c = {
|
||||
tt::hal::i2c::Configuration {
|
||||
|
||||
@ -27,9 +27,9 @@ static bool tdeck_power_on() {
|
||||
}
|
||||
|
||||
bool tdeck_init_power() {
|
||||
ESP_LOGI(TAG, "Power on");
|
||||
ESP_LOGI(TAG, LOG_MESSAGE_POWER_ON_START);
|
||||
if (!tdeck_power_on()) {
|
||||
TT_LOG_E(TAG, "Power on failed");
|
||||
TT_LOG_E(TAG, LOG_MESSAGE_POWER_ON_FAILED);
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
};
|
||||
34
Boards/LilygoTdeck/Source/hal/TdeckSdCard.cpp
Normal file
34
Boards/LilygoTdeck/Source/hal/TdeckSdCard.cpp
Normal 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);
|
||||
}
|
||||
7
Boards/LilygoTdeck/Source/hal/TdeckSdCard.h
Normal file
7
Boards/LilygoTdeck/Source/hal/TdeckSdCard.h
Normal file
@ -0,0 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include "hal/SdCard.h"
|
||||
|
||||
using namespace tt::hal;
|
||||
|
||||
std::shared_ptr<SdCard> createTdeckSdCard();
|
||||
@ -1,12 +1,11 @@
|
||||
#include "M5stackCore2.h"
|
||||
#include "M5stackShared.h"
|
||||
#include "hal/M5stackPower.h"
|
||||
|
||||
extern const tt::hal::Configuration m5stack_core2 = {
|
||||
.initBoot = m5stack_bootstrap,
|
||||
.initLvgl = m5stack_lvgl_init,
|
||||
.createDisplay = createDisplay,
|
||||
.sdcard = &m5stack_sdcard,
|
||||
.sdcard = createM5SdCard(),
|
||||
.power = m5stack_get_power,
|
||||
.i2c = {
|
||||
tt::hal::i2c::Configuration {
|
||||
|
||||
@ -1,12 +1,11 @@
|
||||
#include "M5stackCoreS3.h"
|
||||
#include "M5stackShared.h"
|
||||
#include "hal/M5stackPower.h"
|
||||
|
||||
const tt::hal::Configuration m5stack_cores3 = {
|
||||
.initBoot = m5stack_bootstrap,
|
||||
.initLvgl = m5stack_lvgl_init,
|
||||
.createDisplay = createDisplay,
|
||||
.sdcard = &m5stack_sdcard,
|
||||
.sdcard = createM5SdCard(),
|
||||
.power = m5stack_get_power,
|
||||
.i2c = {
|
||||
tt::hal::i2c::Configuration {
|
||||
|
||||
@ -3,9 +3,8 @@
|
||||
#include "hal/Power.h"
|
||||
#include "hal/M5stackTouch.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_lvgl_init();
|
||||
|
||||
extern const tt::hal::sdcard::SdCard m5stack_sdcard;
|
||||
|
||||
@ -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
|
||||
};
|
||||
31
Boards/M5stackShared/Source/hal/M5stackSdCard.cpp
Normal file
31
Boards/M5stackShared/Source/hal/M5stackSdCard.cpp
Normal 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);
|
||||
}
|
||||
7
Boards/M5stackShared/Source/hal/M5stackSdCard.h
Normal file
7
Boards/M5stackShared/Source/hal/M5stackSdCard.h
Normal file
@ -0,0 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include "hal/SdCard.h"
|
||||
|
||||
using namespace tt::hal;
|
||||
|
||||
std::shared_ptr<SdCard> createM5SdCard();
|
||||
@ -5,11 +5,10 @@
|
||||
#include "src/lv_init.h"
|
||||
#include "SdlDisplay.h"
|
||||
#include "SdlKeyboard.h"
|
||||
#include "SimulatorSdCard.h"
|
||||
|
||||
#define TAG "hardware"
|
||||
|
||||
extern const tt::hal::sdcard::SdCard simulatorSdcard;
|
||||
|
||||
static bool initBoot() {
|
||||
lv_init();
|
||||
lvgl_task_start();
|
||||
@ -31,7 +30,7 @@ extern const tt::hal::Configuration hardware = {
|
||||
.initBoot = initBoot,
|
||||
.createDisplay = createDisplay,
|
||||
.createKeyboard = createKeyboard,
|
||||
.sdcard = &simulatorSdcard,
|
||||
.sdcard = std::make_shared<SimulatorSdCard>(),
|
||||
.power = simulatorPower,
|
||||
.i2c = {
|
||||
tt::hal::i2c::Configuration {
|
||||
|
||||
26
Boards/Simulator/Source/hal/SimulatorSdCard.h
Normal file
26
Boards/Simulator/Source/hal/SimulatorSdCard.h
Normal 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;
|
||||
}
|
||||
};
|
||||
|
||||
@ -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
|
||||
};
|
||||
@ -1,4 +1,4 @@
|
||||
#include "Config.h"
|
||||
#include "YellowConfig.h"
|
||||
#include "TactilityCore.h"
|
||||
#include "hal/YellowTouchConstants.h"
|
||||
#include <driver/spi_common.h>
|
||||
@ -6,6 +6,8 @@
|
||||
#define TAG "twodotfour_bootstrap"
|
||||
|
||||
static bool init_i2c() {
|
||||
TT_LOG_I(TAG, LOG_MESSAGE_I2C_INIT_START);
|
||||
|
||||
const i2c_config_t i2c_conf = {
|
||||
.mode = I2C_MODE_MASTER,
|
||||
.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) {
|
||||
TT_LOG_E(TAG, "i2c config failed");
|
||||
TT_LOG_E(TAG, LOG_MESSAGE_I2C_INIT_CONFIG_FAILED );
|
||||
return false;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@ -31,6 +33,8 @@ static bool init_i2c() {
|
||||
}
|
||||
|
||||
static bool init_spi2() {
|
||||
TT_LOG_I(TAG, LOG_MESSAGE_SPI_INIT_START_FMT, SPI2_HOST);
|
||||
|
||||
const spi_bus_config_t bus_config = {
|
||||
.mosi_io_num = TWODOTFOUR_SPI2_PIN_MOSI,
|
||||
.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) {
|
||||
TT_LOG_E(TAG, "SPI bus init failed");
|
||||
TT_LOG_E(TAG, LOG_MESSAGE_SPI_INIT_FAILED_FMT, SPI2_HOST);
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -49,17 +53,26 @@ static bool init_spi2() {
|
||||
}
|
||||
|
||||
static bool init_spi3() {
|
||||
TT_LOG_I(TAG, LOG_MESSAGE_SPI_INIT_START_FMT, SPI3_HOST);
|
||||
|
||||
const spi_bus_config_t bus_config = {
|
||||
.mosi_io_num = TWODOTFOUR_SPI3_PIN_MOSI,
|
||||
.miso_io_num = TWODOTFOUR_SPI3_PIN_MISO,
|
||||
.sclk_io_num = TWODOTFOUR_SPI3_PIN_SCLK,
|
||||
.quadwp_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) {
|
||||
TT_LOG_E(TAG, "SPI bus init failed");
|
||||
TT_LOG_E(TAG, LOG_MESSAGE_SPI_INIT_FAILED_FMT, SPI3_HOST);
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -67,23 +80,5 @@ static bool init_spi3() {
|
||||
}
|
||||
|
||||
bool twodotfour_boot() {
|
||||
TT_LOG_I(TAG, "Init I2C");
|
||||
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;
|
||||
return init_i2c() && init_spi2() && init_spi3();
|
||||
}
|
||||
|
||||
@ -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
|
||||
};
|
||||
@ -1,16 +1,15 @@
|
||||
#include "YellowBoard.h"
|
||||
#include "hal/YellowDisplay.h"
|
||||
#include "hal/YellowSdCard.h"
|
||||
|
||||
bool twodotfour_lvgl_init();
|
||||
bool twodotfour_boot();
|
||||
|
||||
extern const tt::hal::sdcard::SdCard twodotfour_sdcard;
|
||||
|
||||
const tt::hal::Configuration yellow_board_24inch_cap = {
|
||||
.initBoot = &twodotfour_boot,
|
||||
.initLvgl = &twodotfour_lvgl_init,
|
||||
.createDisplay = createDisplay,
|
||||
.sdcard = &twodotfour_sdcard,
|
||||
.sdcard = createYellowSdCard(),
|
||||
.power = nullptr,
|
||||
.i2c = {
|
||||
tt::hal::i2c::Configuration {
|
||||
|
||||
@ -15,12 +15,3 @@
|
||||
#define TWODOTFOUR_SPI3_PIN_MOSI GPIO_NUM_23
|
||||
#define TWODOTFOUR_SPI3_PIN_MISO GPIO_NUM_19
|
||||
#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
|
||||
31
Boards/YellowBoard/Source/hal/YellowSdCard.cpp
Normal file
31
Boards/YellowBoard/Source/hal/YellowSdCard.cpp
Normal 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);
|
||||
}
|
||||
|
||||
8
Boards/YellowBoard/Source/hal/YellowSdCard.h
Normal file
8
Boards/YellowBoard/Source/hal/YellowSdCard.h
Normal file
@ -0,0 +1,8 @@
|
||||
#pragma once
|
||||
|
||||
#include "hal/SdCard.h"
|
||||
|
||||
using namespace tt::hal;
|
||||
|
||||
std::shared_ptr<SdCard> createYellowSdCard();
|
||||
|
||||
@ -36,6 +36,7 @@ cd Libraries/lvgl
|
||||
find src/ -name '*.h' | cpio -pdm $find_target_dir
|
||||
cd -
|
||||
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_conf/lv_conf_kconfig.h $lvgl_library_path/Include/lv_conf.h
|
||||
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
# Bugs
|
||||
- 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.
|
||||
- ESP32 (CYD) memory issues (or any device without PSRAM):
|
||||
- Boot app doesn't show logo
|
||||
@ -10,22 +9,21 @@
|
||||
- When no PSRAM is available, use simplified desktop buttons
|
||||
- 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)
|
||||
- WiFi details "forget" button should be hidden when WiFi credentials are not stores yet.
|
||||
- Clean up static_cast when casting to base class.
|
||||
|
||||
# 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)
|
||||
- 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.
|
||||
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
|
||||
- Audio player app
|
||||
- Audio recording app
|
||||
- Files app: file operations: rename, delete, copy, paste (long press?), create folder
|
||||
- T-Deck: Use knob for UI selection
|
||||
- Logging to disk/etc.
|
||||
- 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
|
||||
- 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)
|
||||
- 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.
|
||||
@ -43,6 +41,10 @@
|
||||
- Make firmwares available via web serial website
|
||||
- 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.
|
||||
- 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
|
||||
- USB implementation to make device act as mass storage device.
|
||||
|
||||
@ -1,9 +1,8 @@
|
||||
#include <stddef.h>
|
||||
#include "TactilityC/app/App.h"
|
||||
#include "TactilityC/lvgl/Toolbar.h"
|
||||
#include "tt_app_manifest.h"
|
||||
#include "tt_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)
|
||||
*/
|
||||
static void onShow(AppContextHandle context, lv_obj_t* parent) {
|
||||
|
||||
@ -5,18 +5,17 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
set(CMAKE_CXX_EXTENSIONS OFF)
|
||||
|
||||
if (DEFINED ENV{ESP_IDF_VERSION})
|
||||
file(GLOB_RECURSE SOURCE_FILES Source/*.c*) # TODO: Fix
|
||||
|
||||
idf_component_register(
|
||||
SRCS ${SOURCE_FILES}
|
||||
INCLUDE_DIRS "src/"
|
||||
SRC_DIRS "Source/"
|
||||
INCLUDE_DIRS "Source/"
|
||||
PRIV_INCLUDE_DIRS "Private/"
|
||||
REQUIRES lvgl
|
||||
)
|
||||
|
||||
add_definitions(-DESP_PLATFORM)
|
||||
else()
|
||||
file(GLOB SOURCES "src/*.c*")
|
||||
file(GLOB HEADERS "src/*.h*")
|
||||
file(GLOB SOURCES "Source/*.c*")
|
||||
file(GLOB HEADERS "Source/*.h*")
|
||||
|
||||
add_library(lv_screenshot STATIC)
|
||||
|
||||
@ -26,8 +25,8 @@ else()
|
||||
)
|
||||
|
||||
target_include_directories(lv_screenshot
|
||||
PRIVATE private
|
||||
PUBLIC src
|
||||
PRIVATE Private
|
||||
PUBLIC Source
|
||||
)
|
||||
|
||||
target_link_libraries(lv_screenshot
|
||||
|
||||
@ -1,25 +1,29 @@
|
||||
#include "lv_screenshot.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);
|
||||
|
||||
bool lv_screenshot_create(lv_obj_t* obj, lv_color_format_t cf, lv_100ask_screenshot_sv_t screenshot_sv, const char* filename) {
|
||||
lv_draw_buf_t* snapshot = lv_snapshot_take(obj, cf);
|
||||
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, LV_COLOR_FORMAT_RGB888);
|
||||
|
||||
if (snapshot) {
|
||||
data_pre_processing(snapshot, LV_COLOR_DEPTH, screenshot_sv);
|
||||
|
||||
bool success = false;
|
||||
if (screenshot_sv == LV_100ASK_SCREENSHOT_SV_PNG) {
|
||||
if (LV_COLOR_DEPTH == 16) {
|
||||
lv_screenshot_save_png_file(snapshot->data, snapshot->header.w, snapshot->header.h, 24, filename);
|
||||
} else if (LV_COLOR_DEPTH == 32) {
|
||||
lv_screenshot_save_png_file(snapshot->data, snapshot->header.w, snapshot->header.h, 32, filename);
|
||||
}
|
||||
data_pre_processing(snapshot, 24, screenshot_sv);
|
||||
success = lv_screenshot_save_png_file(snapshot->data, snapshot->header.w, snapshot->header.h, 24, filename);
|
||||
} else if (screenshot_sv == LV_100ASK_SCREENSHOT_SV_BMP) {
|
||||
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);
|
||||
return true;
|
||||
return success;
|
||||
}
|
||||
|
||||
return false;
|
||||
@ -45,16 +49,21 @@ static void data_pre_processing(lv_draw_buf_t* snapshot, uint16_t bpp, lv_100ask
|
||||
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;
|
||||
uint32_t count = 0;
|
||||
uint32_t pixel_byte_gap = bpp / 8;
|
||||
for (int w = 0; w < snapshot->header.w; w++) {
|
||||
for (int h = 0; h < snapshot->header.h; h++) {
|
||||
tmp_data = *(snapshot->data + count);
|
||||
*(uint8_t*)(snapshot->data + count) = *(snapshot->data + count + 2);
|
||||
*(uint8_t*)(snapshot->data + count + 2) = tmp_data;
|
||||
count += 4;
|
||||
count += pixel_byte_gap;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@ -1,8 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
#include "lvgl.h"
|
||||
#include <stdbool.h>
|
||||
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
@ -14,7 +12,7 @@ typedef enum {
|
||||
LV_100ASK_SCREENSHOT_SV_LAST
|
||||
} 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
|
||||
} /*extern "C"*/
|
||||
96
Libraries/lv_screenshot/Source/save_bmp.c
Normal file
96
Libraries/lv_screenshot/Source/save_bmp.c
Normal 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;
|
||||
}
|
||||
@ -1,11 +1,11 @@
|
||||
#include "save_png.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) {
|
||||
if (bpp == 32) {
|
||||
return lodepng_encode32_file(filename, image, w, h);
|
||||
return lodepng_encode32_file(filename, image, w, h) == 0;
|
||||
} else if (bpp == 24) {
|
||||
return lodepng_encode24_file(filename, image, w, h);
|
||||
}
|
||||
return lodepng_encode24_file(filename, image, w, h) == 0;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@ -2,6 +2,7 @@
|
||||
|
||||
#include "app/AppManifest.h"
|
||||
#include "app/AppInstance.h"
|
||||
#include "EventFlag.h"
|
||||
#include "MessageQueue.h"
|
||||
#include "Pubsub.h"
|
||||
#include "Thread.h"
|
||||
@ -10,6 +11,7 @@
|
||||
#include "RtosCompatSemaphore.h"
|
||||
#include <stack>
|
||||
#include <utility>
|
||||
#include <DispatcherThread.h>
|
||||
|
||||
namespace tt::service::loader {
|
||||
|
||||
@ -53,98 +55,46 @@ typedef struct {
|
||||
|
||||
// region LoaderMessage
|
||||
|
||||
typedef enum {
|
||||
LoaderMessageTypeNone,
|
||||
LoaderMessageTypeAppStart,
|
||||
LoaderMessageTypeAppStop,
|
||||
LoaderMessageTypeServiceStop,
|
||||
} LoaderMessageType;
|
||||
|
||||
class LoaderMessageAppStart {
|
||||
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::shared_ptr<const Bundle> _Nullable parameters;
|
||||
|
||||
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) :
|
||||
id(id),
|
||||
parameters(std::move(parameters))
|
||||
{}
|
||||
};
|
||||
|
||||
typedef struct {
|
||||
LoaderStatus value;
|
||||
} LoaderMessageLoaderStatusResult;
|
||||
~LoaderMessageAppStart() = default;
|
||||
|
||||
typedef struct {
|
||||
bool value;
|
||||
} LoaderMessageBoolResult;
|
||||
std::shared_ptr<EventFlag> getApiLockEventFlag() { return api_lock; }
|
||||
|
||||
class LoaderMessage {
|
||||
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;
|
||||
uint32_t getApiLockEventFlagValue() { return 1; }
|
||||
|
||||
struct {
|
||||
union {
|
||||
// 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;
|
||||
}
|
||||
void onProcessed() {
|
||||
api_lock->set(1);
|
||||
}
|
||||
};
|
||||
|
||||
// endregion LoaderMessage
|
||||
|
||||
struct Loader {
|
||||
Thread* thread;
|
||||
std::shared_ptr<PubSub> pubsub_internal = 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);
|
||||
std::stack<app::AppInstance*> app_stack;
|
||||
std::unique_ptr<DispatcherThread> dispatcherThread = std::make_unique<DispatcherThread>("loader_dispatcher", 6144); // Files app requires ~5k
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
@ -17,19 +17,19 @@ static const Configuration* config_instance = nullptr;
|
||||
namespace service {
|
||||
namespace gui { 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; }
|
||||
#endif
|
||||
namespace statusbar { extern const ServiceManifest manifest; }
|
||||
}
|
||||
|
||||
static const std::vector<const service::ServiceManifest*> system_services = {
|
||||
&service::loader::manifest,
|
||||
&service::gui::manifest, // depends on loader service
|
||||
#ifndef ESP_PLATFORM // Screenshots don't work yet on ESP32
|
||||
&service::screenshot::manifest,
|
||||
&service::statusbar::manifest,
|
||||
#if TT_FEATURE_SCREENSHOT_ENABLED
|
||||
&service::screenshot::manifest
|
||||
#endif
|
||||
&service::statusbar::manifest
|
||||
};
|
||||
|
||||
// endregion
|
||||
@ -44,23 +44,24 @@ namespace app {
|
||||
namespace files { extern const AppManifest manifest; }
|
||||
namespace gpio { 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 i2cscanner { extern const AppManifest manifest; }
|
||||
namespace i2csettings { extern const AppManifest manifest; }
|
||||
namespace inputdialog { extern const AppManifest manifest; }
|
||||
namespace power { extern const AppManifest manifest; }
|
||||
namespace selectiondialog { extern const AppManifest manifest; }
|
||||
namespace settings { extern const AppManifest manifest; }
|
||||
namespace systeminfo { extern const AppManifest manifest; }
|
||||
namespace textviewer { extern const AppManifest manifest; }
|
||||
namespace wifiapsettings { extern const AppManifest manifest; }
|
||||
namespace wificonnect { 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
|
||||
extern const AppManifest elfWrapperManifest;
|
||||
namespace crashdiagnostics { extern const AppManifest manifest; }
|
||||
#else
|
||||
namespace app::screenshot { extern const AppManifest manifest; }
|
||||
#endif
|
||||
}
|
||||
|
||||
@ -77,6 +78,7 @@ static const std::vector<const app::AppManifest*> system_apps = {
|
||||
&app::gpio::manifest,
|
||||
&app::i2cscanner::manifest,
|
||||
&app::i2csettings::manifest,
|
||||
&app::inputdialog::manifest,
|
||||
&app::imageviewer::manifest,
|
||||
&app::settings::manifest,
|
||||
&app::selectiondialog::manifest,
|
||||
@ -85,11 +87,12 @@ static const std::vector<const app::AppManifest*> system_apps = {
|
||||
&app::wifiapsettings::manifest,
|
||||
&app::wificonnect::manifest,
|
||||
&app::wifimanage::manifest,
|
||||
#if TT_FEATURE_SCREENSHOT_ENABLED
|
||||
&app::screenshot::manifest,
|
||||
#endif
|
||||
#ifdef ESP_PLATFORM
|
||||
&app::crashdiagnostics::manifest,
|
||||
&app::elfWrapperManifest, // For hot-loading ELF apps
|
||||
#else
|
||||
&app::screenshot::manifest, // Screenshots don't work yet on ESP32
|
||||
#endif
|
||||
};
|
||||
|
||||
@ -166,12 +169,7 @@ void run(const Configuration& config) {
|
||||
register_user_apps(config.apps);
|
||||
|
||||
TT_LOG_I(TAG, "init starting desktop app");
|
||||
service::loader::startApp(app::boot::manifest.id, true);
|
||||
|
||||
if (config.autoStartAppId) {
|
||||
TT_LOG_I(TAG, "init auto-starting %s", config.autoStartAppId);
|
||||
service::loader::startApp(config.autoStartAppId, true);
|
||||
}
|
||||
service::loader::startApp(app::boot::manifest.id);
|
||||
|
||||
TT_LOG_I(TAG, "init complete");
|
||||
|
||||
|
||||
@ -1,7 +1,17 @@
|
||||
#pragma once
|
||||
|
||||
#ifdef ESP_PLATFORM
|
||||
#include "sdkconfig.h"
|
||||
#endif
|
||||
|
||||
#include "TactilityHeadlessConfig.h"
|
||||
|
||||
#define TT_CONFIG_APPS_LIMIT 32
|
||||
#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)
|
||||
|
||||
#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
|
||||
|
||||
@ -16,13 +16,13 @@ static size_t elfManifestSetCount = 0;
|
||||
std::unique_ptr<uint8_t[]> elfFileData;
|
||||
esp_elf_t elf;
|
||||
|
||||
bool startElfApp(const char* filePath) {
|
||||
TT_LOG_I(TAG, "Starting ELF %s", filePath);
|
||||
bool startElfApp(const std::string& filePath) {
|
||||
TT_LOG_I(TAG, "Starting ELF %s", filePath.c_str());
|
||||
|
||||
assert(elfFileData == nullptr);
|
||||
|
||||
size_t size = 0;
|
||||
elfFileData = file::readBinary(filePath, size);
|
||||
elfFileData = file::readBinary(filePath.c_str(), size);
|
||||
if (elfFileData == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -6,7 +6,7 @@
|
||||
|
||||
namespace tt::app {
|
||||
|
||||
bool startElfApp(const char* filePath);
|
||||
bool startElfApp(const std::string& filePath);
|
||||
|
||||
void setElfAppManifest(const AppManifest& manifest);
|
||||
|
||||
|
||||
@ -49,7 +49,6 @@ static std::string getTitleParameter(std::shared_ptr<const Bundle> bundle) {
|
||||
|
||||
static void onButtonClicked(lv_event_t* 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));
|
||||
TT_LOG_I(TAG, "Selected item at index %d", index);
|
||||
tt::app::AppContext* app = service::loader::getCurrentApp();
|
||||
@ -58,14 +57,13 @@ static void onButtonClicked(lv_event_t* e) {
|
||||
app->setResult(app::ResultOk, bundle);
|
||||
service::loader::stopApp();
|
||||
}
|
||||
}
|
||||
|
||||
static void createButton(lv_obj_t* parent, const std::string& text, size_t index) {
|
||||
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_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) {
|
||||
@ -78,6 +76,7 @@ static void onShow(AppContext& app, lv_obj_t* parent) {
|
||||
|
||||
lv_obj_t* message_label = lv_label_create(parent);
|
||||
lv_obj_align(message_label, LV_ALIGN_CENTER, 0, 0);
|
||||
lv_obj_set_width(message_label, LV_PCT(80));
|
||||
|
||||
std::string message;
|
||||
if (parameters->optString(PARAMETER_BUNDLE_KEY_MESSAGE, message)) {
|
||||
|
||||
@ -9,19 +9,16 @@
|
||||
namespace tt::app::applist {
|
||||
|
||||
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));
|
||||
service::loader::startApp(manifest->id, false);
|
||||
}
|
||||
}
|
||||
|
||||
static void createAppWidget(const AppManifest* manifest, void* parent) {
|
||||
tt_check(parent);
|
||||
auto* list = reinterpret_cast<lv_obj_t*>(parent);
|
||||
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_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) {
|
||||
|
||||
@ -4,29 +4,33 @@
|
||||
#include "app/AppContext.h"
|
||||
#include "app/display/DisplaySettings.h"
|
||||
#include "hal/Display.h"
|
||||
#include "kernel/PanicHandler.h"
|
||||
#include "service/loader/Loader.h"
|
||||
#include "lvgl/Style.h"
|
||||
|
||||
#include "lvgl.h"
|
||||
#include "Tactility.h"
|
||||
|
||||
#ifdef ESP_PLATFORM
|
||||
#include "kernel/PanicHandler.h"
|
||||
#include "sdkconfig.h"
|
||||
#else
|
||||
#define CONFIG_TT_SPLASH_DURATION 0
|
||||
#endif
|
||||
|
||||
#define TAG "Boot"
|
||||
|
||||
namespace tt::app::boot {
|
||||
|
||||
static int32_t threadCallback(void* context);
|
||||
static int32_t bootThreadCallback(void* context);
|
||||
static void startNextApp();
|
||||
|
||||
struct Data {
|
||||
Data() : thread("boot", 4096, threadCallback, this) {}
|
||||
Data() : thread("boot", 4096, bootThreadCallback, this) {}
|
||||
|
||||
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();
|
||||
|
||||
auto* lvgl_display = lv_display_get_default();
|
||||
@ -46,18 +50,33 @@ static int32_t threadCallback(TT_UNUSED void* context) {
|
||||
}
|
||||
|
||||
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
|
||||
esp_reset_reason_t reason = esp_reset_reason();
|
||||
if (reason == ESP_RST_PANIC) {
|
||||
tt::service::loader::startApp("CrashDiagnostics");
|
||||
} else {
|
||||
tt::service::loader::startApp("Desktop");
|
||||
tt::service::loader::startApp(next_app);
|
||||
}
|
||||
#else
|
||||
tt::service::loader::startApp("Desktop");
|
||||
tt::service::loader::startApp(next_app);
|
||||
#endif
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void onShow(TT_UNUSED AppContext& app, lv_obj_t* parent) {
|
||||
|
||||
@ -21,7 +21,7 @@ static void onShow(TT_UNUSED AppContext& app, lv_obj_t* parent) {
|
||||
auto* display = lv_obj_get_display(parent);
|
||||
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);
|
||||
lv_label_set_text(top_label, "Oops! We've crashed ..."); // TODO: Funny messages
|
||||
lv_obj_align(top_label, LV_ALIGN_TOP_MID, 0, 2);
|
||||
|
||||
@ -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);
|
||||
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_opa(button_image, LV_OPA_COVER, 0);
|
||||
|
||||
|
||||
@ -1,97 +1,99 @@
|
||||
#include "FileUtils.h"
|
||||
#include "TactilityCore.h"
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <bits/stdc++.h>
|
||||
|
||||
namespace tt::app::files {
|
||||
|
||||
#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) {
|
||||
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 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 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 right_is_dir = right.d_type == TT_DT_DIR || right.d_type == TT_DT_CHR;
|
||||
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 {
|
||||
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(
|
||||
const char* path,
|
||||
struct dirent*** output,
|
||||
ScandirFilter _Nullable filter,
|
||||
ScandirSort _Nullable sort
|
||||
const std::string& path,
|
||||
std::vector<dirent>& outList,
|
||||
ScandirFilter _Nullable filterMethod,
|
||||
ScandirSort _Nullable sortMethod
|
||||
) {
|
||||
DIR* dir = opendir(path);
|
||||
DIR* dir = opendir(path.c_str());
|
||||
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;
|
||||
}
|
||||
|
||||
*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;
|
||||
bool out_of_memory = false;
|
||||
while ((current_entry = readdir(dir)) != nullptr) {
|
||||
if (filter(current_entry) == 0) {
|
||||
dirent_array[next_dirent_index] = static_cast<dirent*>(malloc(sizeof(struct dirent)));
|
||||
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);
|
||||
if (filterMethod(current_entry) == 0) {
|
||||
outList.push_back(*current_entry);
|
||||
}
|
||||
}
|
||||
|
||||
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");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
#pragma once
|
||||
|
||||
#include <dirent.h>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace tt::app::files {
|
||||
|
||||
@ -26,19 +28,13 @@ enum {
|
||||
#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 (*ScandirSort)(const struct dirent**, const struct dirent**);
|
||||
typedef bool (*ScandirSort)(const struct dirent&, const struct dirent&);
|
||||
|
||||
/**
|
||||
* 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);
|
||||
bool dirent_sort_alpha_and_type(const struct dirent& left, const struct dirent& right);
|
||||
|
||||
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.
|
||||
*
|
||||
* @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] sort an optional sorting function
|
||||
* @return the amount of items that were stored in "output" or -1 when an error occurred
|
||||
*/
|
||||
int scandir(
|
||||
const char* path,
|
||||
struct dirent*** output,
|
||||
const std::string& path,
|
||||
std::vector<dirent>& outList,
|
||||
ScandirFilter _Nullable filter,
|
||||
ScandirSort _Nullable sort
|
||||
);
|
||||
|
||||
bool isSupportedExecutableFile(const std::string& filename);
|
||||
bool isSupportedImageFile(const std::string& filename);
|
||||
bool isSupportedTextFile(const std::string& filename);
|
||||
|
||||
} // namespace
|
||||
|
||||
@ -1,21 +1,8 @@
|
||||
#include "FilesData.h"
|
||||
#include "Files.h"
|
||||
#include "app/AppContext.h"
|
||||
#include "Tactility.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 <cstring>
|
||||
|
||||
namespace tt::app::files {
|
||||
|
||||
@ -24,232 +11,22 @@ namespace tt::app::files {
|
||||
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. */
|
||||
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) {
|
||||
auto data = std::static_pointer_cast<Data>(app.getData());
|
||||
|
||||
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);
|
||||
auto files = std::static_pointer_cast<Files>(app.getData());
|
||||
files->onShow(parent);
|
||||
}
|
||||
|
||||
static void onStart(AppContext& app) {
|
||||
auto* test = new uint32_t;
|
||||
delete test;
|
||||
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, "/");
|
||||
auto files = std::make_shared<Files>();
|
||||
app.setData(files);
|
||||
}
|
||||
|
||||
app.setData(data);
|
||||
static void onResult(AppContext& app, Result result, const Bundle& bundle) {
|
||||
auto files = std::static_pointer_cast<Files>(app.getData());
|
||||
files->onResult(result, bundle);
|
||||
}
|
||||
|
||||
// endregion Lifecycle
|
||||
|
||||
extern const AppManifest manifest = {
|
||||
.id = "Files",
|
||||
.name = "Files",
|
||||
@ -257,6 +34,7 @@ extern const AppManifest manifest = {
|
||||
.type = TypeHidden,
|
||||
.onStart = onStart,
|
||||
.onShow = onShow,
|
||||
.onResult = onResult
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
33
Tactility/Source/app/files/Files.h
Normal file
33
Tactility/Source/app/files/Files.h
Normal 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
|
||||
@ -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
|
||||
@ -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
|
||||
106
Tactility/Source/app/files/State.cpp
Normal file
106
Tactility/Source/app/files/State.cpp
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
74
Tactility/Source/app/files/State.h
Normal file
74
Tactility/Source/app/files/State.h
Normal 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;
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
329
Tactility/Source/app/files/View.cpp
Normal file
329
Tactility/Source/app/files/View.cpp
Normal 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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
40
Tactility/Source/app/files/View.h
Normal file
40
Tactility/Source/app/files/View.h
Normal 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);
|
||||
};
|
||||
|
||||
}
|
||||
@ -89,7 +89,7 @@ static void updateViews(std::shared_ptr<Data> data) {
|
||||
|
||||
tt_check(data->mutex.release() == TtStatusOk);
|
||||
} 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);
|
||||
lvgl::unlock();
|
||||
} 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);
|
||||
} 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_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_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_align(scan_button_label, LV_ALIGN_CENTER, 0, 0);
|
||||
lv_label_set_text(scan_button_label, START_SCAN_TEXT);
|
||||
|
||||
@ -22,7 +22,7 @@ static bool getPort(std::shared_ptr<Data> data, i2c_port_t* port) {
|
||||
tt_assert(data->mutex.release() == TtStatusOk);
|
||||
return true;
|
||||
} else {
|
||||
TT_LOG_W(TAG, "getPort lock");
|
||||
TT_LOG_W(TAG, LOG_MESSAGE_MUTEX_LOCK_FAILED_FMT, "getPort");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@ -33,7 +33,7 @@ static bool addAddressToList(std::shared_ptr<Data> data, uint8_t address) {
|
||||
tt_assert(data->mutex.release() == TtStatusOk);
|
||||
return true;
|
||||
} else {
|
||||
TT_LOG_W(TAG, "addAddressToList lock");
|
||||
TT_LOG_W(TAG, LOG_MESSAGE_MUTEX_LOCK_FAILED_FMT, "addAddressToList");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@ -58,7 +58,7 @@ static void onScanTimer(TT_UNUSED std::shared_ptr<void> context) {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
TT_LOG_W(TAG, "onScanTimer lock");
|
||||
TT_LOG_W(TAG, LOG_MESSAGE_MUTEX_LOCK_FAILED_FMT, "onScanTimer");
|
||||
break;
|
||||
}
|
||||
|
||||
@ -82,7 +82,7 @@ bool hasScanThread(std::shared_ptr<Data> data) {
|
||||
return has_thread;
|
||||
} else {
|
||||
// Unsafe way
|
||||
TT_LOG_W(TAG, "hasScanTimer lock");
|
||||
TT_LOG_W(TAG, LOG_MESSAGE_MUTEX_LOCK_FAILED_FMT, "hasScanTimer");
|
||||
return data->scanTimer != nullptr;
|
||||
}
|
||||
}
|
||||
@ -107,7 +107,7 @@ void startScanning(std::shared_ptr<Data> data) {
|
||||
data->scanTimer->start(10);
|
||||
tt_check(data->mutex.release() == TtStatusOk);
|
||||
} 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;
|
||||
tt_check(data->mutex.release() == TtStatusOk);
|
||||
} else {
|
||||
TT_LOG_E(TAG, "Acquire mutex failed");
|
||||
TT_LOG_E(TAG, LOG_MESSAGE_MUTEX_LOCK_FAILED);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
120
Tactility/Source/app/inputdialog/InputDialog.cpp
Normal file
120
Tactility/Source/app/inputdialog/InputDialog.cpp
Normal 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
|
||||
};
|
||||
|
||||
}
|
||||
20
Tactility/Source/app/inputdialog/InputDialog.h
Normal file
20
Tactility/Source/app/inputdialog/InputDialog.h
Normal 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);
|
||||
}
|
||||
@ -1,3 +1,7 @@
|
||||
#include "TactilityConfig.h"
|
||||
|
||||
#if TT_FEATURE_SCREENSHOT_ENABLED
|
||||
|
||||
#include "ScreenshotUi.h"
|
||||
#include <memory>
|
||||
|
||||
@ -5,17 +9,17 @@ namespace tt::app::screenshot {
|
||||
|
||||
static void onShow(AppContext& app, lv_obj_t* parent) {
|
||||
auto ui = std::static_pointer_cast<ScreenshotUi>(app.getData());
|
||||
create_ui(app, ui, parent);
|
||||
ui->createWidgets(app, parent);
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
extern const AppManifest manifest = {
|
||||
.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,
|
||||
.type = TypeSystem,
|
||||
.onStart = onStart,
|
||||
@ -23,3 +27,5 @@ extern const AppManifest manifest = {
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
#endif
|
||||
|
||||
@ -1,18 +1,23 @@
|
||||
#include "TactilityConfig.h"
|
||||
|
||||
#if TT_FEATURE_SCREENSHOT_ENABLED
|
||||
|
||||
#include "ScreenshotUi.h"
|
||||
|
||||
#include "TactilityCore.h"
|
||||
#include "hal/sdcard/Sdcard.h"
|
||||
#include "hal/SdCard.h"
|
||||
#include "service/gui/Gui.h"
|
||||
#include "service/loader/Loader.h"
|
||||
#include "service/screenshot/Screenshot.h"
|
||||
#include "lvgl/Toolbar.h"
|
||||
#include "TactilityHeadless.h"
|
||||
#include "lvgl/LvglSync.h"
|
||||
|
||||
namespace tt::app::screenshot {
|
||||
|
||||
#define TAG "screenshot_ui"
|
||||
|
||||
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. */
|
||||
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();
|
||||
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;
|
||||
}
|
||||
|
||||
if (service::screenshot::isStarted()) {
|
||||
if (service->isTaskStarted()) {
|
||||
TT_LOG_I(TAG, "Stop screenshot");
|
||||
service::screenshot::stop();
|
||||
service->stop();
|
||||
} else {
|
||||
uint32_t selected = lv_dropdown_get_selected(ui->mode_dropdown);
|
||||
const char* path = lv_textarea_get_text(ui->path_textarea);
|
||||
uint32_t selected = lv_dropdown_get_selected(modeDropdown);
|
||||
const char* path = lv_textarea_get_text(pathTextArea);
|
||||
if (selected == 0) {
|
||||
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);
|
||||
if (delay > 0) {
|
||||
service::screenshot::startTimed(path, delay, 1);
|
||||
service->startTimed(path, delay, 1);
|
||||
} else {
|
||||
TT_LOG_W(TAG, "Ignored screenshot start because delay was 0");
|
||||
}
|
||||
} else {
|
||||
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) {
|
||||
lv_obj_t* label = ui->start_stop_button_label;
|
||||
if (service::screenshot::isStarted()) {
|
||||
void ScreenshotUi::updateScreenshotMode() {
|
||||
auto service = service::screenshot::optScreenshotService();
|
||||
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");
|
||||
} else {
|
||||
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
|
||||
lv_obj_remove_flag(ui->timer_wrapper, LV_OBJ_FLAG_HIDDEN);
|
||||
lv_obj_remove_flag(timerWrapper, LV_OBJ_FLAG_HIDDEN);
|
||||
} 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);
|
||||
}
|
||||
|
||||
void ScreenshotUi::createModeSettingWidgets(lv_obj_t* parent) {
|
||||
auto service = service::screenshot::optScreenshotService();
|
||||
if (service == nullptr) {
|
||||
TT_LOG_E(TAG, "Service not found/running");
|
||||
return;
|
||||
}
|
||||
|
||||
static void create_mode_setting_ui(std::shared_ptr<ScreenshotUi> ui, lv_obj_t* parent) {
|
||||
lv_obj_t* mode_wrapper = lv_obj_create(parent);
|
||||
auto* mode_wrapper = lv_obj_create(parent);
|
||||
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_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_obj_align(mode_label, LV_ALIGN_LEFT_MID, 0, 0);
|
||||
|
||||
lv_obj_t* mode_dropdown = lv_dropdown_create(mode_wrapper);
|
||||
lv_dropdown_set_options(mode_dropdown, "Timer\nApp start");
|
||||
lv_obj_align_to(mode_dropdown, mode_label, LV_ALIGN_OUT_RIGHT_MID, 8, 0);
|
||||
lv_obj_add_event_cb(mode_dropdown, on_mode_set, LV_EVENT_VALUE_CHANGED, nullptr);
|
||||
ui->mode_dropdown = mode_dropdown;
|
||||
service::screenshot::Mode mode = service::screenshot::getMode();
|
||||
modeDropdown = lv_dropdown_create(mode_wrapper);
|
||||
lv_dropdown_set_options(modeDropdown, "Timer\nApp start");
|
||||
lv_obj_align_to(modeDropdown, mode_label, LV_ALIGN_OUT_RIGHT_MID, 8, 0);
|
||||
lv_obj_add_event_cb(modeDropdown, onModeSetCallback, LV_EVENT_VALUE_CHANGED, nullptr);
|
||||
service::screenshot::Mode mode = service->getMode();
|
||||
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_t* button_label = lv_label_create(button);
|
||||
lv_obj_align(button_label, LV_ALIGN_CENTER, 0, 0);
|
||||
ui->start_stop_button_label = button_label;
|
||||
lv_obj_add_event_cb(button, &on_start_pressed, LV_EVENT_CLICKED, nullptr);
|
||||
lv_obj_add_event_cb(button, &onStartPressedCallback, LV_EVENT_SHORT_CLICKED, nullptr);
|
||||
startStopButtonLabel = lv_label_create(button);
|
||||
lv_obj_align(startStopButtonLabel, LV_ALIGN_CENTER, 0, 0);
|
||||
}
|
||||
|
||||
static void create_path_ui(std::shared_ptr<ScreenshotUi> ui, lv_obj_t* parent) {
|
||||
lv_obj_t* path_wrapper = lv_obj_create(parent);
|
||||
void ScreenshotUi::createFilePathWidgets(lv_obj_t* parent) {
|
||||
auto* path_wrapper = lv_obj_create(parent);
|
||||
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_border_width(path_wrapper, 0, 0);
|
||||
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_pad_all(label_wrapper, 0, 0);
|
||||
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_obj_align(path_label, LV_ALIGN_LEFT_MID, 0, 0);
|
||||
|
||||
lv_obj_t* path_textarea = lv_textarea_create(path_wrapper);
|
||||
lv_textarea_set_one_line(path_textarea, true);
|
||||
lv_obj_set_flex_grow(path_textarea, 1);
|
||||
ui->path_textarea = path_textarea;
|
||||
pathTextArea = lv_textarea_create(path_wrapper);
|
||||
lv_textarea_set_one_line(pathTextArea, true);
|
||||
lv_obj_set_flex_grow(pathTextArea, 1);
|
||||
if (kernel::getPlatform() == kernel::PlatformEsp) {
|
||||
if (hal::sdcard::getState() == hal::sdcard::StateMounted) {
|
||||
lv_textarea_set_text(path_textarea, "A:/sdcard");
|
||||
auto sdcard = tt::hal::getConfiguration().sdcard;
|
||||
if (sdcard != nullptr && sdcard->getState() == hal::SdCard::StateMounted) {
|
||||
lv_textarea_set_text(pathTextArea, "A:/sdcard");
|
||||
} else {
|
||||
lv_textarea_set_text(path_textarea, "Error: no SD card");
|
||||
lv_textarea_set_text(pathTextArea, "Error: no SD card");
|
||||
}
|
||||
} 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) {
|
||||
lv_obj_t* timer_wrapper = lv_obj_create(parent);
|
||||
lv_obj_set_size(timer_wrapper, LV_PCT(100), LV_SIZE_CONTENT);
|
||||
lv_obj_set_style_pad_all(timer_wrapper, 0, 0);
|
||||
lv_obj_set_style_border_width(timer_wrapper, 0, 0);
|
||||
ui->timer_wrapper = timer_wrapper;
|
||||
void ScreenshotUi::createTimerSettingsWidgets(lv_obj_t* parent) {
|
||||
timerWrapper = lv_obj_create(parent);
|
||||
lv_obj_set_size(timerWrapper, LV_PCT(100), LV_SIZE_CONTENT);
|
||||
lv_obj_set_style_pad_all(timerWrapper, 0, 0);
|
||||
lv_obj_set_style_border_width(timerWrapper, 0, 0);
|
||||
|
||||
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_style_pad_all(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_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_pad_all(delay_label_wrapper, 0, 0);
|
||||
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_obj_align(delay_label, LV_ALIGN_LEFT_MID, 0, 0);
|
||||
|
||||
lv_obj_t* delay_textarea = lv_textarea_create(delay_wrapper);
|
||||
lv_textarea_set_one_line(delay_textarea, true);
|
||||
lv_textarea_set_accepted_chars(delay_textarea, "0123456789");
|
||||
lv_textarea_set_text(delay_textarea, "10");
|
||||
lv_obj_set_flex_grow(delay_textarea, 1);
|
||||
ui->delay_textarea = delay_textarea;
|
||||
delayTextArea = lv_textarea_create(delay_wrapper);
|
||||
lv_textarea_set_one_line(delayTextArea, true);
|
||||
lv_textarea_set_accepted_chars(delayTextArea, "0123456789");
|
||||
lv_textarea_set_text(delayTextArea, "10");
|
||||
lv_obj_set_flex_grow(delayTextArea, 1);
|
||||
|
||||
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_pad_all(delay_unit_label_wrapper, 0, 0);
|
||||
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_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_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_t* wrapper = lv_obj_create(parent);
|
||||
auto* wrapper = lv_obj_create(parent);
|
||||
lv_obj_set_width(wrapper, LV_PCT(100));
|
||||
lv_obj_set_flex_grow(wrapper, 1);
|
||||
lv_obj_set_style_border_width(wrapper, 0, 0);
|
||||
lv_obj_set_flex_flow(wrapper, LV_FLEX_FLOW_COLUMN);
|
||||
|
||||
create_mode_setting_ui(ui, wrapper);
|
||||
create_path_ui(ui, wrapper);
|
||||
create_timer_settings_ui(ui, wrapper);
|
||||
createModeSettingWidgets(wrapper);
|
||||
createFilePathWidgets(wrapper);
|
||||
createTimerSettingsWidgets(wrapper);
|
||||
|
||||
service::gui::keyboardAddTextArea(ui->delay_textarea);
|
||||
service::gui::keyboardAddTextArea(ui->path_textarea);
|
||||
service::gui::keyboardAddTextArea(delayTextArea);
|
||||
service::gui::keyboardAddTextArea(pathTextArea);
|
||||
|
||||
update_mode(ui);
|
||||
updateScreenshotMode();
|
||||
|
||||
if (!updateTimer->isRunning()) {
|
||||
updateTimer->start(500 / portTICK_PERIOD_MS);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
#endif
|
||||
@ -1,3 +1,8 @@
|
||||
#include "Timer.h"
|
||||
#include "TactilityConfig.h"
|
||||
|
||||
#if TT_FEATURE_SCREENSHOT_ENABLED
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "app/AppContext.h"
|
||||
@ -5,14 +10,33 @@
|
||||
|
||||
namespace tt::app::screenshot {
|
||||
|
||||
typedef struct {
|
||||
lv_obj_t* mode_dropdown;
|
||||
lv_obj_t* path_textarea;
|
||||
lv_obj_t* start_stop_button_label;
|
||||
lv_obj_t* timer_wrapper;
|
||||
lv_obj_t* delay_textarea;
|
||||
} ScreenshotUi;
|
||||
class ScreenshotUi {
|
||||
|
||||
lv_obj_t* modeDropdown = nullptr;
|
||||
lv_obj_t* pathTextArea = nullptr;
|
||||
lv_obj_t* startStopButtonLabel = nullptr;
|
||||
lv_obj_t* timerWrapper = nullptr;
|
||||
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
|
||||
|
||||
#endif
|
||||
|
||||
@ -46,8 +46,6 @@ static std::string getTitleParameter(std::shared_ptr<const Bundle> bundle) {
|
||||
}
|
||||
|
||||
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));
|
||||
TT_LOG_I(TAG, "Selected item at index %d", index);
|
||||
tt::app::AppContext* app = service::loader::getCurrentApp();
|
||||
@ -56,12 +54,11 @@ static void onListItemSelected(lv_event_t* e) {
|
||||
app->setResult(app::ResultOk, bundle);
|
||||
service::loader::stopApp();
|
||||
}
|
||||
}
|
||||
|
||||
static void createChoiceItem(void* parent, const std::string& title, size_t index) {
|
||||
auto* list = static_cast<lv_obj_t*>(parent);
|
||||
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) {
|
||||
|
||||
@ -9,19 +9,16 @@
|
||||
namespace tt::app::settings {
|
||||
|
||||
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));
|
||||
service::loader::startApp(manifest->id);
|
||||
}
|
||||
}
|
||||
|
||||
static void createWidget(const AppManifest* manifest, void* parent) {
|
||||
tt_check(parent);
|
||||
auto* list = (lv_obj_t*)parent;
|
||||
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_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) {
|
||||
|
||||
@ -100,7 +100,7 @@ static void onShow(AppContext& app, lv_obj_t* parent) {
|
||||
lv_obj_t* forget_button = lv_button_create(wrapper);
|
||||
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_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_align(forget_button_label, LV_ALIGN_CENTER, 0, 0);
|
||||
lv_label_set_text(forget_button_label, "Forget");
|
||||
@ -112,9 +112,10 @@ static void onShow(AppContext& app, lv_obj_t* parent) {
|
||||
} else {
|
||||
lv_obj_remove_state(auto_connect_switch, LV_STATE_CHECKED);
|
||||
}
|
||||
|
||||
} 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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -106,7 +106,7 @@ void View::createBottomButtons(lv_obj_t* parent) {
|
||||
lv_obj_t* connect_label = lv_label_create(connect_button);
|
||||
lv_label_set_text(connect_label, "Connect");
|
||||
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
|
||||
|
||||
@ -82,7 +82,7 @@ void WifiConnect::requestViewUpdate() {
|
||||
view.update();
|
||||
lvgl::unlock();
|
||||
} else {
|
||||
TT_LOG_E(TAG, "Failed to lock lvgl");
|
||||
TT_LOG_E(TAG, LOG_MESSAGE_MUTEX_LOCK_FAILED_FMT, "LVGL");
|
||||
}
|
||||
}
|
||||
unlock();
|
||||
|
||||
@ -76,7 +76,7 @@ static void showDetails(lv_event_t* event) {
|
||||
|
||||
void View::createSsidListItem(const service::wifi::WifiApRecord& record, bool isConnecting) {
|
||||
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_size(wrapper, LV_PCT(100), LV_SIZE_CONTENT);
|
||||
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_size(info_wrapper, 36, 36);
|
||||
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_t* info_label = lv_label_create(info_wrapper);
|
||||
|
||||
@ -76,7 +76,7 @@ void WifiManage::requestViewUpdate() {
|
||||
view.update();
|
||||
lvgl::unlock();
|
||||
} else {
|
||||
TT_LOG_E(TAG, "failed to lock lvgl");
|
||||
TT_LOG_E(TAG, LOG_MESSAGE_MUTEX_LOCK_FAILED_FMT, "LVGL");
|
||||
}
|
||||
}
|
||||
unlock();
|
||||
|
||||
@ -29,4 +29,24 @@ void unlock() {
|
||||
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
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include "Lockable.h"
|
||||
|
||||
#include <memory>
|
||||
|
||||
namespace tt::lvgl {
|
||||
|
||||
@ -12,4 +14,6 @@ bool isSyncSet();
|
||||
bool lock(uint32_t timeout_ticks);
|
||||
void unlock();
|
||||
|
||||
std::shared_ptr<Lockable> getLvglSyncLockable();
|
||||
|
||||
} // namespace
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
#define LV_USE_PRIVATE_API 1 // For actual lv_obj_t declaration
|
||||
|
||||
#include "Toolbar.h"
|
||||
|
||||
#include "service/loader/Loader.h"
|
||||
@ -6,8 +7,6 @@
|
||||
#include "lvgl/Style.h"
|
||||
#include "Spinner.h"
|
||||
|
||||
#define SPINNER_HEIGHT TOOLBAR_HEIGHT
|
||||
|
||||
namespace tt::lvgl {
|
||||
|
||||
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) {
|
||||
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_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;
|
||||
uint8_t id = toolbar->action_count;
|
||||
tt_check(toolbar->action_count < TOOLBAR_ACTION_LIMIT, "max actions reached");
|
||||
toolbar->action_count++;
|
||||
|
||||
lv_obj_t* action_button = lv_button_create(toolbar->action_container);
|
||||
lv_obj_set_size(action_button, TOOLBAR_HEIGHT - 4, TOOLBAR_HEIGHT - 4);
|
||||
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_image_set_src(action_button_image, icon);
|
||||
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;
|
||||
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_right(widget, 4, 0);
|
||||
return widget;
|
||||
}
|
||||
|
||||
|
||||
@ -59,7 +59,7 @@ void redraw(Gui* gui) {
|
||||
// Unlock GUI and LVGL
|
||||
lvgl::unlock();
|
||||
} else {
|
||||
TT_LOG_E(TAG, "failed to lock lvgl");
|
||||
TT_LOG_E(TAG, LOG_MESSAGE_MUTEX_LOCK_FAILED_FMT, "LVGL");
|
||||
}
|
||||
|
||||
unlock();
|
||||
|
||||
@ -1,5 +1,3 @@
|
||||
#include "Tactility.h"
|
||||
#include <Mutex.h>
|
||||
#include "app/AppManifest.h"
|
||||
#include "app/ManifestRegistry.h"
|
||||
#include "service/ServiceManifest.h"
|
||||
@ -9,95 +7,72 @@
|
||||
|
||||
#ifdef ESP_PLATFORM
|
||||
#include "esp_heap_caps.h"
|
||||
#include "TactilityHeadless.h"
|
||||
|
||||
#else
|
||||
#include "lvgl/LvglSync.h"
|
||||
#include "TactilityHeadless.h"
|
||||
#endif
|
||||
|
||||
namespace tt::service::loader {
|
||||
|
||||
#define TAG "loader"
|
||||
#define LOADER_EVENT_FLAG 1
|
||||
|
||||
typedef struct {
|
||||
LoaderEventType type;
|
||||
} LoaderEventInternal;
|
||||
|
||||
// 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_alloc() {
|
||||
assert(loader_singleton == nullptr);
|
||||
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;
|
||||
}
|
||||
|
||||
static void loader_free() {
|
||||
tt_assert(loader_singleton != nullptr);
|
||||
delete loader_singleton->thread;
|
||||
delete loader_singleton;
|
||||
loader_singleton = nullptr;
|
||||
}
|
||||
|
||||
static void loader_lock() {
|
||||
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) {
|
||||
void startApp(const std::string& id, bool blocking, std::shared_ptr<const Bundle> parameters) {
|
||||
TT_LOG_I(TAG, "Start app %s", id.c_str());
|
||||
tt_assert(loader_singleton);
|
||||
|
||||
LoaderMessageLoaderStatusResult result = {
|
||||
.value = LoaderStatusOk
|
||||
};
|
||||
auto message = std::make_shared<LoaderMessageAppStart>(id, parameters);
|
||||
loader_singleton->dispatcherThread->dispatch(onStartAppMessage, message);
|
||||
|
||||
auto* start_message = new LoaderMessageAppStart(id, parameters);
|
||||
LoaderMessage message(start_message, result);
|
||||
|
||||
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) {
|
||||
auto event_flag = message->getApiLockEventFlag();
|
||||
if (blocking) {
|
||||
/* TODO: Check if task id is not the LVGL one,
|
||||
because otherwise this fails as the apps starting logic will try to lock lvgl
|
||||
to update the UI and fail. */
|
||||
event_flag->wait(LOADER_EVENT_FLAG);
|
||||
delete event_flag;
|
||||
event_flag->wait(message->getApiLockEventFlagValue());
|
||||
}
|
||||
|
||||
return result.value;
|
||||
}
|
||||
|
||||
void stopApp() {
|
||||
TT_LOG_I(TAG, "Stop app");
|
||||
tt_check(loader_singleton);
|
||||
LoaderMessage message(LoaderMessageTypeAppStop);
|
||||
loader_singleton->queue.put(&message, TtWaitForever);
|
||||
loader_singleton->dispatcherThread->dispatch(onStopAppMessage, nullptr);
|
||||
}
|
||||
|
||||
app::AppContext* _Nullable getCurrentApp() {
|
||||
tt_assert(loader_singleton);
|
||||
loader_lock();
|
||||
if (loader_singleton->mutex.lock(10 / portTICK_PERIOD_MS)) {
|
||||
app::AppInstance* app = loader_singleton->app_stack.top();
|
||||
loader_unlock();
|
||||
loader_singleton->mutex.unlock();
|
||||
return dynamic_cast<app::AppContext*>(app);
|
||||
} else {
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
std::shared_ptr<PubSub> getPubsub() {
|
||||
@ -108,7 +83,7 @@ std::shared_ptr<PubSub> getPubsub() {
|
||||
return loader_singleton->pubsub_external;
|
||||
}
|
||||
|
||||
static const char* app_state_to_string(app::State state) {
|
||||
static const char* appStateToString(app::State state) {
|
||||
switch (state) {
|
||||
case app::StateInitial:
|
||||
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::State old_state = app.getState();
|
||||
|
||||
TT_LOG_I(
|
||||
TAG,
|
||||
"app \"%s\" state: %s -> %s",
|
||||
"App \"%s\" state: %s -> %s",
|
||||
manifest.id.c_str(),
|
||||
app_state_to_string(old_state),
|
||||
app_state_to_string(state)
|
||||
appStateToString(old_state),
|
||||
appStateToString(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,
|
||||
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 new_app = new app::AppInstance(*manifest, parameters);
|
||||
new_app->mutableFlags().showStatusbar = (manifest->type != app::TypeBoot);
|
||||
|
||||
loader_singleton->app_stack.push(new_app);
|
||||
app_transition_to_state(*new_app, app::StateInitial);
|
||||
app_transition_to_state(*new_app, app::StateStarted);
|
||||
transitionAppToState(*new_app, app::StateInitial);
|
||||
transitionAppToState(*new_app, app::StateStarted);
|
||||
|
||||
// We might have to hide the previous app first
|
||||
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);
|
||||
|
||||
loader_unlock();
|
||||
transitionAppToState(*new_app, app::StateShowing);
|
||||
|
||||
LoaderEventInternal event_internal = {.type = LoaderEventTypeApplicationStarted};
|
||||
tt_pubsub_publish(loader_singleton->pubsub_internal, &event_internal);
|
||||
@ -218,7 +196,16 @@ static LoaderStatus loader_do_start_app_with_manifest(
|
||||
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,
|
||||
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());
|
||||
return LoaderStatusErrorUnknownApp;
|
||||
} 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() {
|
||||
loader_lock();
|
||||
auto scoped_lock = loader_singleton->mutex.scoped();
|
||||
if (!scoped_lock->lock(50 / portTICK_PERIOD_MS)) {
|
||||
return;
|
||||
}
|
||||
|
||||
size_t original_stack_size = loader_singleton->app_stack.size();
|
||||
|
||||
if (original_stack_size == 0) {
|
||||
loader_unlock();
|
||||
TT_LOG_E(TAG, "Stop app: no app running");
|
||||
return;
|
||||
}
|
||||
@ -249,7 +239,6 @@ static void do_stop_app() {
|
||||
app::AppInstance* app_to_stop = loader_singleton->app_stack.top();
|
||||
|
||||
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");
|
||||
return;
|
||||
}
|
||||
@ -257,8 +246,8 @@ static void do_stop_app() {
|
||||
std::unique_ptr<app::ResultHolder> result_holder = std::move(app_to_stop->getResult());
|
||||
|
||||
const app::AppManifest& manifest = app_to_stop->getManifest();
|
||||
app_transition_to_state(*app_to_stop, app::StateHiding);
|
||||
app_transition_to_state(*app_to_stop, app::StateStopped);
|
||||
transitionAppToState(*app_to_stop, app::StateHiding);
|
||||
transitionAppToState(*app_to_stop, app::StateStopped);
|
||||
|
||||
loader_singleton->app_stack.pop();
|
||||
delete app_to_stop;
|
||||
@ -273,12 +262,14 @@ static void do_stop_app() {
|
||||
if (!loader_singleton->app_stack.empty()) {
|
||||
app_to_resume = loader_singleton->app_stack.top();
|
||||
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;
|
||||
}
|
||||
|
||||
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};
|
||||
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
|
||||
|
||||
static void loader_start(TT_UNUSED ServiceContext& service) {
|
||||
tt_check(loader_singleton == nullptr);
|
||||
loader_singleton = loader_alloc();
|
||||
|
||||
loader_singleton->thread->setPriority(THREAD_PRIORITY_SERVICE);
|
||||
loader_singleton->thread->start();
|
||||
loader_singleton->dispatcherThread->start();
|
||||
}
|
||||
|
||||
static void loader_stop(TT_UNUSED ServiceContext& service) {
|
||||
tt_check(loader_singleton != nullptr);
|
||||
|
||||
// Send stop signal to thread and wait for thread to finish
|
||||
loader_lock();
|
||||
LoaderMessage message(LoaderMessageTypeServiceStop);
|
||||
loader_singleton->queue.put(&message, TtWaitForever);
|
||||
loader_unlock();
|
||||
if (!loader_singleton->mutex.lock(2000 / portTICK_PERIOD_MS)) {
|
||||
TT_LOG_W(TAG, LOG_MESSAGE_MUTEX_LOCK_FAILED_FMT, "loader_stop");
|
||||
}
|
||||
loader_singleton->dispatcherThread->stop();
|
||||
|
||||
loader_singleton->thread->join();
|
||||
delete loader_singleton->thread;
|
||||
loader_singleton->mutex.unlock();
|
||||
|
||||
loader_free();
|
||||
loader_singleton = nullptr;
|
||||
|
||||
@ -23,9 +23,8 @@ typedef enum {
|
||||
* @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] 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.
|
||||
|
||||
@ -1,9 +1,10 @@
|
||||
#include "TactilityConfig.h"
|
||||
|
||||
#if TT_FEATURE_SCREENSHOT_ENABLED
|
||||
|
||||
#include "Screenshot.h"
|
||||
#include <cstdlib>
|
||||
#include <memory>
|
||||
|
||||
#include "Mutex.h"
|
||||
#include "ScreenshotTask.h"
|
||||
#include "service/ServiceContext.h"
|
||||
#include "service/ServiceRegistry.h"
|
||||
|
||||
@ -13,105 +14,84 @@ namespace tt::service::screenshot {
|
||||
|
||||
extern const ServiceManifest manifest;
|
||||
|
||||
struct ServiceData {
|
||||
Mutex mutex;
|
||||
task::ScreenshotTask* task = nullptr;
|
||||
Mode mode = ScreenshotModeNone;
|
||||
|
||||
~ServiceData() {
|
||||
if (task) {
|
||||
task::free(task);
|
||||
std::shared_ptr<ScreenshotService> _Nullable optScreenshotService() {
|
||||
ServiceContext* context = service::findServiceById(manifest.id);
|
||||
if (context != nullptr) {
|
||||
return std::static_pointer_cast<ScreenshotService>(context->getData());
|
||||
} else {
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
void lock() {
|
||||
tt_check(mutex.acquire(TtWaitForever) == TtStatusOk);
|
||||
}
|
||||
|
||||
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");
|
||||
void ScreenshotService::startApps(const char* path) {
|
||||
auto scoped_lockable = mutex.scoped();
|
||||
if (!scoped_lockable->lock(50 / portTICK_PERIOD_MS)) {
|
||||
TT_LOG_W(TAG, LOG_MESSAGE_MUTEX_LOCK_FAILED);
|
||||
return;
|
||||
}
|
||||
|
||||
auto data = std::static_pointer_cast<ServiceData>(service->getData());
|
||||
data->lock();
|
||||
if (data->task == nullptr) {
|
||||
data->task = task::alloc();
|
||||
data->mode = ScreenshotModeApps;
|
||||
task::startApps(data->task, path);
|
||||
if (task == nullptr || task->isFinished()) {
|
||||
task = std::make_unique<ScreenshotTask>();
|
||||
mode = ScreenshotModeApps;
|
||||
task->startApps(path);
|
||||
} 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) {
|
||||
_Nullable auto* service = findServiceById(manifest.id);
|
||||
if (service == nullptr) {
|
||||
TT_LOG_E(TAG, "Service not found");
|
||||
void ScreenshotService::startTimed(const char* path, uint8_t delay_in_seconds, uint8_t amount) {
|
||||
auto scoped_lockable = mutex.scoped();
|
||||
if (!scoped_lockable->lock(50 / portTICK_PERIOD_MS)) {
|
||||
TT_LOG_W(TAG, LOG_MESSAGE_MUTEX_LOCK_FAILED);
|
||||
return;
|
||||
}
|
||||
|
||||
auto data = std::static_pointer_cast<ServiceData>(service->getData());
|
||||
data->lock();
|
||||
if (data->task == nullptr) {
|
||||
data->task = task::alloc();
|
||||
data->mode = ScreenshotModeTimed;
|
||||
task::startTimed(data->task, path, delay_in_seconds, amount);
|
||||
if (task == nullptr || task->isFinished()) {
|
||||
task = std::make_unique<ScreenshotTask>();
|
||||
mode = ScreenshotModeTimed;
|
||||
task->startTimed(path, delay_in_seconds, amount);
|
||||
} else {
|
||||
TT_LOG_E(TAG, "Screenshot task already running");
|
||||
TT_LOG_W(TAG, "Screenshot task already running");
|
||||
}
|
||||
data->unlock();
|
||||
}
|
||||
|
||||
void stop() {
|
||||
_Nullable ServiceContext* service = findServiceById(manifest.id);
|
||||
if (service == nullptr) {
|
||||
TT_LOG_E(TAG, "Service not found");
|
||||
void ScreenshotService::stop() {
|
||||
auto scoped_lockable = mutex.scoped();
|
||||
if (!scoped_lockable->lock(50 / portTICK_PERIOD_MS)) {
|
||||
TT_LOG_W(TAG, LOG_MESSAGE_MUTEX_LOCK_FAILED);
|
||||
return;
|
||||
}
|
||||
|
||||
auto data = std::static_pointer_cast<ServiceData>(service->getData());
|
||||
data->lock();
|
||||
if (data->task != nullptr) {
|
||||
task::stop(data->task);
|
||||
task::free(data->task);
|
||||
data->task = nullptr;
|
||||
data->mode = ScreenshotModeNone;
|
||||
if (task != nullptr) {
|
||||
task = nullptr;
|
||||
mode = ScreenshotModeNone;
|
||||
} else {
|
||||
TT_LOG_E(TAG, "Screenshot task not running");
|
||||
TT_LOG_W(TAG, "Screenshot task not running");
|
||||
}
|
||||
data->unlock();
|
||||
}
|
||||
|
||||
Mode getMode() {
|
||||
_Nullable auto* service = findServiceById(manifest.id);
|
||||
if (service == nullptr) {
|
||||
TT_LOG_E(TAG, "Service not found");
|
||||
Mode ScreenshotService::getMode() {
|
||||
auto scoped_lockable = mutex.scoped();
|
||||
if (!scoped_lockable->lock(50 / portTICK_PERIOD_MS)) {
|
||||
TT_LOG_W(TAG, LOG_MESSAGE_MUTEX_LOCK_FAILED);
|
||||
return ScreenshotModeNone;
|
||||
} else {
|
||||
auto data = std::static_pointer_cast<ServiceData>(service->getData());
|
||||
data->lock();
|
||||
Mode mode = data->mode;
|
||||
data->unlock();
|
||||
}
|
||||
|
||||
return mode;
|
||||
}
|
||||
|
||||
bool ScreenshotService::isTaskStarted() {
|
||||
auto* current_task = task.get();
|
||||
if (current_task == nullptr) {
|
||||
return false;
|
||||
} else {
|
||||
return !current_task->isFinished();
|
||||
}
|
||||
}
|
||||
|
||||
bool isStarted() {
|
||||
return getMode() != ScreenshotModeNone;
|
||||
}
|
||||
|
||||
static void onStart(ServiceContext& service) {
|
||||
auto data = std::make_shared<ServiceData>();
|
||||
service.setData(data);
|
||||
static void onStart(ServiceContext& serviceContext) {
|
||||
auto service = std::make_shared<ScreenshotService>();
|
||||
serviceContext.setData(service);
|
||||
}
|
||||
|
||||
extern const ServiceManifest manifest = {
|
||||
@ -120,3 +100,5 @@ extern const ServiceManifest manifest = {
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
#endif
|
||||
@ -1,5 +1,11 @@
|
||||
#pragma once
|
||||
|
||||
#include "Mutex.h"
|
||||
#include "ScreenshotTask.h"
|
||||
#include "TactilityConfig.h"
|
||||
|
||||
#if TT_FEATURE_SCREENSHOT_ENABLED
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace tt::service::screenshot {
|
||||
@ -10,22 +16,23 @@ typedef enum {
|
||||
ScreenshotModeApps
|
||||
} 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
|
||||
* @param path the path to store the screenshots in
|
||||
*/
|
||||
void startApps(const char* path);
|
||||
class ScreenshotService {
|
||||
Mutex mutex;
|
||||
std::unique_ptr<ScreenshotTask> task;
|
||||
Mode mode = ScreenshotModeNone;
|
||||
|
||||
void stop();
|
||||
public:
|
||||
|
||||
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
|
||||
|
||||
#endif
|
||||
|
||||
@ -1,184 +1,184 @@
|
||||
#include "TactilityConfig.h"
|
||||
|
||||
#if TT_FEATURE_SCREENSHOT_ENABLED
|
||||
|
||||
#include <cstring>
|
||||
#include "ScreenshotTask.h"
|
||||
#include "lv_screenshot.h"
|
||||
|
||||
#include "app/AppContext.h"
|
||||
#include "Mutex.h"
|
||||
#include "TactilityCore.h"
|
||||
#include "Thread.h"
|
||||
#include "service/loader/Loader.h"
|
||||
#include "lvgl/LvglSync.h"
|
||||
|
||||
namespace tt::service::screenshot::task {
|
||||
namespace tt::service::screenshot {
|
||||
|
||||
#define TAG "screenshot_task"
|
||||
|
||||
#define TASK_WORK_TYPE_DELAY 1
|
||||
#define TASK_WORK_TYPE_APPS 2
|
||||
|
||||
#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);
|
||||
ScreenshotTask::~ScreenshotTask() {
|
||||
if (thread) {
|
||||
stop();
|
||||
}
|
||||
}
|
||||
|
||||
static void task_unlock(ScreenshotTaskData* data) {
|
||||
tt_check(data->mutex.release() == TtStatusOk);
|
||||
bool ScreenshotTask::isInterrupted() {
|
||||
auto scoped_lockable = mutex.scoped();
|
||||
if (!scoped_lockable->lock(50 / portTICK_PERIOD_MS)) {
|
||||
TT_LOG_W(TAG, LOG_MESSAGE_MUTEX_LOCK_FAILED);
|
||||
return true;
|
||||
}
|
||||
|
||||
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) {
|
||||
task_lock(data);
|
||||
bool interrupted = data->interrupted;
|
||||
task_unlock(data);
|
||||
return interrupted;
|
||||
}
|
||||
|
||||
static int32_t screenshot_task(void* context) {
|
||||
auto* data = static_cast<ScreenshotTaskData*>(context);
|
||||
|
||||
bool interrupted = false;
|
||||
uint8_t screenshots_taken = 0;
|
||||
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);
|
||||
bool ScreenshotTask::isFinished() {
|
||||
auto scoped_lockable = mutex.scoped();
|
||||
if (!scoped_lockable->lock(50 / portTICK_PERIOD_MS)) {
|
||||
TT_LOG_W(TAG, LOG_MESSAGE_MUTEX_LOCK_FAILED);
|
||||
return false;
|
||||
}
|
||||
return finished;
|
||||
}
|
||||
|
||||
if (is_interrupted(data)) {
|
||||
break;
|
||||
void ScreenshotTask::setFinished() {
|
||||
auto scoped_lockable = mutex.scoped();
|
||||
scoped_lockable->lock(TtWaitForever);
|
||||
finished = true;
|
||||
}
|
||||
|
||||
screenshots_taken++;
|
||||
char filename[SCREENSHOT_PATH_LIMIT + 32];
|
||||
sprintf(filename, "%s/screenshot-%d.png", data->work.path, screenshots_taken);
|
||||
lvgl::lock(TtWaitForever);
|
||||
if (lv_screenshot_create(lv_scr_act(), LV_COLOR_FORMAT_NATIVE, LV_100ASK_SCREENSHOT_SV_PNG, filename)){
|
||||
static void makeScreenshot(const char* filename) {
|
||||
if (lvgl::lock(50 / portTICK_PERIOD_MS)) {
|
||||
if (lv_screenshot_create(lv_scr_act(), 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();
|
||||
} 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
|
||||
}
|
||||
} else if (data->work.type == TASK_WORK_TYPE_APPS) {
|
||||
}
|
||||
} else if (work.type == TASK_WORK_TYPE_APPS) {
|
||||
app::AppContext* _Nullable app = loader::getCurrentApp();
|
||||
if (app) {
|
||||
const app::AppManifest& manifest = app->getManifest();
|
||||
if (manifest.id != last_app_id) {
|
||||
kernel::delayMillis(100);
|
||||
last_app_id = manifest.id;
|
||||
|
||||
char filename[SCREENSHOT_PATH_LIMIT + 32];
|
||||
sprintf(filename, "%s/screenshot-%s.png", data->work.path, manifest.id.c_str());
|
||||
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);
|
||||
} else {
|
||||
TT_LOG_E(TAG, "Screenshot not saved to %s", filename);
|
||||
}
|
||||
lvgl::unlock();
|
||||
sprintf(filename, "%s/screenshot-%s.png", work.path, manifest.id.c_str());
|
||||
makeScreenshot(filename);
|
||||
}
|
||||
}
|
||||
// Ensure the LVGL widgets are rendered as the app just started
|
||||
kernel::delayMillis(250);
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
setFinished();
|
||||
}
|
||||
|
||||
static void task_start(ScreenshotTaskData* data) {
|
||||
task_lock(data);
|
||||
tt_check(data->thread == nullptr);
|
||||
data->thread = new Thread(
|
||||
void ScreenshotTask::taskStart() {
|
||||
auto scoped_lockable = mutex.scoped();
|
||||
if (!scoped_lockable->lock(50 / portTICK_PERIOD_MS)) {
|
||||
TT_LOG_E(TAG, LOG_MESSAGE_MUTEX_LOCK_FAILED);
|
||||
return;
|
||||
}
|
||||
|
||||
tt_check(thread == nullptr);
|
||||
thread = new Thread(
|
||||
"screenshot",
|
||||
8192,
|
||||
&screenshot_task,
|
||||
data
|
||||
&screenshotTaskCallback,
|
||||
this
|
||||
);
|
||||
data->thread->start();
|
||||
task_unlock(data);
|
||||
thread->start();
|
||||
}
|
||||
|
||||
void startApps(ScreenshotTask* task, const char* path) {
|
||||
void ScreenshotTask::startApps(const char* path) {
|
||||
tt_check(strlen(path) < (SCREENSHOT_PATH_LIMIT - 1));
|
||||
auto* data = static_cast<ScreenshotTaskData*>(task);
|
||||
task_lock(data);
|
||||
if (data->thread == nullptr) {
|
||||
data->interrupted = false;
|
||||
data->work.type = TASK_WORK_TYPE_APPS;
|
||||
strcpy(data->work.path, path);
|
||||
task_start(data);
|
||||
|
||||
auto scoped_lockable = mutex.scoped();
|
||||
if (!scoped_lockable->lock(50 / portTICK_PERIOD_MS)) {
|
||||
TT_LOG_E(TAG, LOG_MESSAGE_MUTEX_LOCK_FAILED);
|
||||
return;
|
||||
}
|
||||
|
||||
if (thread == nullptr) {
|
||||
interrupted = false;
|
||||
work.type = TASK_WORK_TYPE_APPS;
|
||||
strcpy(work.path, path);
|
||||
taskStart();
|
||||
} else {
|
||||
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));
|
||||
auto* data = static_cast<ScreenshotTaskData*>(task);
|
||||
task_lock(data);
|
||||
if (data->thread == nullptr) {
|
||||
data->interrupted = false;
|
||||
data->work.type = TASK_WORK_TYPE_DELAY;
|
||||
data->work.delay_in_seconds = delay_in_seconds;
|
||||
data->work.amount = amount;
|
||||
strcpy(data->work.path, path);
|
||||
task_start(data);
|
||||
auto scoped_lockable = mutex.scoped();
|
||||
if (!scoped_lockable->lock(50 / portTICK_PERIOD_MS)) {
|
||||
TT_LOG_E(TAG, LOG_MESSAGE_MUTEX_LOCK_FAILED);
|
||||
return;
|
||||
}
|
||||
|
||||
if (thread == nullptr) {
|
||||
interrupted = false;
|
||||
work.type = TASK_WORK_TYPE_DELAY;
|
||||
work.delay_in_seconds = delay_in_seconds;
|
||||
work.amount = amount;
|
||||
strcpy(work.path, path);
|
||||
taskStart();
|
||||
} else {
|
||||
TT_LOG_E(TAG, "Task was already running");
|
||||
}
|
||||
task_unlock(data);
|
||||
}
|
||||
|
||||
void stop(ScreenshotTask* task) {
|
||||
auto* data = static_cast<ScreenshotTaskData*>(task);
|
||||
if (data->thread != nullptr) {
|
||||
task_lock(data);
|
||||
data->interrupted = true;
|
||||
task_unlock(data);
|
||||
void ScreenshotTask::stop() {
|
||||
if (thread != nullptr) {
|
||||
if (mutex.lock(50 / portTICK_PERIOD_MS)) {
|
||||
interrupted = true;
|
||||
tt_check(mutex.unlock());
|
||||
}
|
||||
|
||||
data->thread->join();
|
||||
thread->join();
|
||||
|
||||
task_lock(data);
|
||||
delete data->thread;
|
||||
data->thread = nullptr;
|
||||
task_unlock(data);
|
||||
if (mutex.lock(50 / portTICK_PERIOD_MS)) {
|
||||
delete thread;
|
||||
thread = nullptr;
|
||||
tt_check(mutex.unlock());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
#endif
|
||||
|
||||
@ -1,14 +1,38 @@
|
||||
#include "TactilityConfig.h"
|
||||
|
||||
#if TT_FEATURE_SCREENSHOT_ENABLED
|
||||
|
||||
#pragma once
|
||||
|
||||
#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 {
|
||||
|
||||
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
|
||||
@ -16,17 +40,30 @@ void free(ScreenshotTask* task);
|
||||
* @param delay_in_seconds the delay before starting (and between successive screenshots)
|
||||
* @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
|
||||
* @param task the screenshot task
|
||||
* @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
|
||||
* @param task the screenshot task
|
||||
*/
|
||||
void stop(ScreenshotTask* task);
|
||||
void stop();
|
||||
|
||||
void taskMain();
|
||||
|
||||
bool isFinished();
|
||||
|
||||
private:
|
||||
|
||||
bool isInterrupted();
|
||||
void setFinished();
|
||||
void taskStart();
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
@ -4,11 +4,12 @@
|
||||
#include "Tactility.h"
|
||||
|
||||
#include "hal/Power.h"
|
||||
#include "hal/sdcard/Sdcard.h"
|
||||
#include "hal/SdCard.h"
|
||||
#include "lvgl/Statusbar.h"
|
||||
#include "service/ServiceContext.h"
|
||||
#include "service/wifi/Wifi.h"
|
||||
#include "service/ServiceRegistry.h"
|
||||
#include "TactilityHeadless.h"
|
||||
|
||||
namespace tt::service::statusbar {
|
||||
|
||||
@ -85,20 +86,23 @@ static void update_wifi_icon(std::shared_ptr<ServiceData> data) {
|
||||
|
||||
// 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) {
|
||||
case hal::sdcard::StateMounted:
|
||||
case hal::SdCard::StateMounted:
|
||||
return TT_ASSETS_ICON_SDCARD;
|
||||
case hal::sdcard::StateError:
|
||||
case hal::sdcard::StateUnmounted:
|
||||
case hal::SdCard::StateError:
|
||||
case hal::SdCard::StateUnmounted:
|
||||
case hal::SdCard::StateUnknown:
|
||||
return TT_ASSETS_ICON_SDCARD_ALERT;
|
||||
default:
|
||||
return nullptr;
|
||||
tt_crash("Unhandled SdCard state");
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
if (data->sdcard_last_icon != desired_icon) {
|
||||
lvgl::statusbar_icon_set_image(data->sdcard_icon_id, desired_icon);
|
||||
@ -106,6 +110,7 @@ static void update_sdcard_icon(std::shared_ptr<ServiceData> data) {
|
||||
data->sdcard_last_icon = desired_icon;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// endregion sdcard
|
||||
|
||||
|
||||
19
TactilityC/Source/tt_app_alertdialog.cpp
Normal file
19
TactilityC/Source/tt_app_alertdialog.cpp
Normal 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);
|
||||
}
|
||||
|
||||
}
|
||||
16
TactilityC/Source/tt_app_alertdialog.h
Normal file
16
TactilityC/Source/tt_app_alertdialog.h
Normal 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
|
||||
36
TactilityC/Source/tt_app_context.cpp
Normal file
36
TactilityC/Source/tt_app_context.cpp
Normal 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();
|
||||
}
|
||||
|
||||
}
|
||||
19
TactilityC/Source/tt_app_context.h
Normal file
19
TactilityC/Source/tt_app_context.h
Normal 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
|
||||
@ -1,9 +1,10 @@
|
||||
#include <Check.h>
|
||||
#include "App.h"
|
||||
#include "Log.h"
|
||||
#include "app/ElfApp.h"
|
||||
#include "tt_app_manifest.h"
|
||||
|
||||
#define TAG "tactilityc_app"
|
||||
#include <Check.h>
|
||||
#include <Log.h>
|
||||
#include <app/ElfApp.h>
|
||||
|
||||
#define TAG "tt_app"
|
||||
|
||||
AppOnStart elfOnStart = nullptr;
|
||||
AppOnStop elfOnStop = nullptr;
|
||||
@ -100,7 +101,7 @@ void tt_set_app_manifest(
|
||||
elfOnResult = onResult;
|
||||
tt::app::setElfAppManifest(manifest);
|
||||
#else
|
||||
tt_crash("Not intended for PC");
|
||||
tt_crash("TactilityC is intended for PC/Simulator");
|
||||
#endif
|
||||
}
|
||||
|
||||
@ -1,20 +1,20 @@
|
||||
#pragma once
|
||||
|
||||
#include "lvgl.h"
|
||||
#include "tt_bundle.h"
|
||||
#include <lvgl.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
typedef void* AppContextHandle;
|
||||
typedef void* BundleHandle;
|
||||
|
||||
typedef enum {
|
||||
AppResultOk,
|
||||
AppResultCancelled,
|
||||
AppResultError
|
||||
} Result;
|
||||
|
||||
typedef void* AppContextHandle;
|
||||
|
||||
typedef void (*AppOnStart)(AppContextHandle app);
|
||||
typedef void (*AppOnStop)(AppContextHandle app);
|
||||
typedef void (*AppOnShow)(AppContextHandle app, lv_obj_t* parent);
|
||||
@ -1,4 +1,5 @@
|
||||
#include "app/selectiondialog/SelectionDialog.h"
|
||||
#include "tt_app_selectiondialog.h"
|
||||
#include <app/selectiondialog/SelectionDialog.h>
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
int32_t tt_app_selectiondialog_get_result_index(BundleHandle handle) {
|
||||
return tt::app::selectiondialog::getResultIndex(*(tt::Bundle*)handle);
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,11 +1,15 @@
|
||||
#pragma once
|
||||
|
||||
#include "tt_bundle.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
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
|
||||
}
|
||||
#endif
|
||||
51
TactilityC/Source/tt_bundle.cpp
Normal file
51
TactilityC/Source/tt_bundle.cpp
Normal 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);
|
||||
}
|
||||
|
||||
}
|
||||
29
TactilityC/Source/tt_bundle.h
Normal file
29
TactilityC/Source/tt_bundle.h
Normal 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
|
||||
@ -2,21 +2,86 @@
|
||||
|
||||
#include "elf_symbol.h"
|
||||
|
||||
#include "app/App.h"
|
||||
#include "app/SelectionDialog.h"
|
||||
#include "lvgl/Toolbar.h"
|
||||
#include "TactilityC/lvgl/Spinner.h"
|
||||
#include "tt_app_context.h"
|
||||
#include "tt_app_manifest.h"
|
||||
#include "tt_app_alertdialog.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" {
|
||||
|
||||
const struct esp_elfsym elf_symbols[] {
|
||||
// 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_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_lvgl_toolbar_create),
|
||||
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
|
||||
ESP_ELFSYM_EXPORT(tt_lvgl_spinner_create),
|
||||
// lv_obj
|
||||
@ -1,4 +1,4 @@
|
||||
#include "Spinner.h"
|
||||
#include "tt_lvgl_spinner.h"
|
||||
#include "lvgl/Spinner.h"
|
||||
|
||||
extern "C" {
|
||||
@ -1,6 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
#include "lvgl.h"
|
||||
#include <lvgl.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
@ -1,5 +1,5 @@
|
||||
#include "Toolbar.h"
|
||||
#include "lvgl/Toolbar.h"
|
||||
#include "tt_lvgl_toolbar.h"
|
||||
#include <lvgl/Toolbar.h>
|
||||
|
||||
extern "C" {
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include "lvgl.h"
|
||||
#include "TactilityC/app/App.h"
|
||||
#include <lvgl.h>
|
||||
#include "tt_app_context.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
40
TactilityC/Source/tt_message_queue.cpp
Normal file
40
TactilityC/Source/tt_message_queue.cpp
Normal 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();
|
||||
}
|
||||
|
||||
}
|
||||
23
TactilityC/Source/tt_message_queue.h
Normal file
23
TactilityC/Source/tt_message_queue.h
Normal 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
|
||||
31
TactilityC/Source/tt_mutex.cpp
Normal file
31
TactilityC/Source/tt_mutex.cpp
Normal 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();
|
||||
}
|
||||
|
||||
}
|
||||
24
TactilityC/Source/tt_mutex.h
Normal file
24
TactilityC/Source/tt_mutex.h
Normal 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
|
||||
28
TactilityC/Source/tt_semaphore.cpp
Normal file
28
TactilityC/Source/tt_semaphore.cpp
Normal 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();
|
||||
}
|
||||
|
||||
}
|
||||
20
TactilityC/Source/tt_semaphore.h
Normal file
20
TactilityC/Source/tt_semaphore.h
Normal 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
Loading…
x
Reference in New Issue
Block a user