diff --git a/CMakeLists.txt b/CMakeLists.txt index 149c523f..d1003b4c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -35,6 +35,7 @@ endif() project(tactility-root) add_subdirectory(libs/mlib) +add_subdirectory(libs/lv_screenshot) add_subdirectory(tactility) add_subdirectory(tactility-core) diff --git a/README.md b/README.md index d838c92d..20073c26 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,17 @@ It provides an application framework that is based on code from the [Flipper Zer **Status: Alpha** -![Tactility shown on a Lilygo T-Deck device and on PC](docs/pics/tactility-showcase.jpg) +Tactility features a desktop that can launch apps: + +![screenshot of desktop app](docs/pics/screenshot-desktop.png) ![screenshot of files app](docs/pics/screenshot-files.png) ![screenshot of system info app](docs/pics/screenshot-systeminfo.png) + +Through the Settings app you can connect to Wi-Fi or change the display settings: + +![screenshot of settings app](docs/pics/screenshot-settings.png) ![screenshot of display settings app](docs/pics/screenshot-display.png) + +Play with the built-in apps or build your own! Use one of the supported devices or set up the drivers for your own hardware platform. + +![photo of devices running Tactility](docs/pics/tactility-devices.jpg) Noteworthy features: - Touch UI capabilities (via LVGL) with support for input devices such as on-device trackball or keyboard. @@ -51,7 +61,7 @@ const AppManifest hello_world_app = { }; ``` -![hello world app](docs/pics/hello-world.png) +![hello world app screenshot](docs/pics/screenshot-helloworld.png) ## Supported Hardware diff --git a/app-sim/src/main.c b/app-sim/src/main.c index fdc13e4b..1ccbec1f 100644 --- a/app-sim/src/main.c +++ b/app-sim/src/main.c @@ -1,7 +1,9 @@ #include "hello_world/hello_world.h" #include "tactility.h" +#include "assets.h" #include "FreeRTOS.h" +#include "ui/statusbar.h" #define TAG "main" @@ -18,4 +20,9 @@ void app_main() { }; tt_init(&config); + + // Note: this is just to test the statusbar as Wi-Fi + // and sd card apps are not available for PC + tt_statusbar_icon_add(TT_ASSETS_ICON_SDCARD_ALERT); + tt_statusbar_icon_add(TT_ASSETS_ICON_WIFI_OFF); } diff --git a/docs/ideas.md b/docs/ideas.md index 995055a9..52c7179e 100644 --- a/docs/ideas.md +++ b/docs/ideas.md @@ -7,6 +7,8 @@ - Try out Waveshare S3 120MHz mode for PSRAM (see "enabling 120M PSRAM is necessary" in [docs](https://www.waveshare.com/wiki/ESP32-S3-Touch-LCD-4.3#Other_Notes)) - T-Deck has random sdcard SPI crashes due to sharing bus with screen SPI: make it use the LVGL lock for sdcard operations? - Wi-Fi connect app should show info about connection result +- Check service/app id on registration to see if it is a duplicate id +- Fix screenshot app on ESP32: it currently blocks when allocating memory # Core Ideas - Make a HAL? It would mainly be there to support PC development. It's a lot of effort for supporting what's effectively a dev-only feature. diff --git a/docs/pics/hello-world.png b/docs/pics/hello-world.png deleted file mode 100644 index f4651fb3..00000000 Binary files a/docs/pics/hello-world.png and /dev/null differ diff --git a/docs/pics/screenshot-desktop.png b/docs/pics/screenshot-desktop.png new file mode 100644 index 00000000..cc62f3e6 Binary files /dev/null and b/docs/pics/screenshot-desktop.png differ diff --git a/docs/pics/screenshot-display.png b/docs/pics/screenshot-display.png new file mode 100644 index 00000000..313165f1 Binary files /dev/null and b/docs/pics/screenshot-display.png differ diff --git a/docs/pics/screenshot-files.png b/docs/pics/screenshot-files.png new file mode 100644 index 00000000..1865e392 Binary files /dev/null and b/docs/pics/screenshot-files.png differ diff --git a/docs/pics/screenshot-helloworld.png b/docs/pics/screenshot-helloworld.png new file mode 100644 index 00000000..729c4f6a Binary files /dev/null and b/docs/pics/screenshot-helloworld.png differ diff --git a/docs/pics/screenshot-settings.png b/docs/pics/screenshot-settings.png new file mode 100644 index 00000000..dc616742 Binary files /dev/null and b/docs/pics/screenshot-settings.png differ diff --git a/docs/pics/screenshot-systeminfo.png b/docs/pics/screenshot-systeminfo.png new file mode 100644 index 00000000..91413d4c Binary files /dev/null and b/docs/pics/screenshot-systeminfo.png differ diff --git a/docs/pics/tactility-devices.jpg b/docs/pics/tactility-devices.jpg new file mode 100644 index 00000000..c5c4bb78 Binary files /dev/null and b/docs/pics/tactility-devices.jpg differ diff --git a/docs/pics/tactility-showcase.jpg b/docs/pics/tactility-showcase.jpg deleted file mode 100644 index 05db464a..00000000 Binary files a/docs/pics/tactility-showcase.jpg and /dev/null differ diff --git a/libs/lv_screenshot/CMakeLists.txt b/libs/lv_screenshot/CMakeLists.txt new file mode 100644 index 00000000..ab5d9524 --- /dev/null +++ b/libs/lv_screenshot/CMakeLists.txt @@ -0,0 +1,30 @@ +cmake_minimum_required(VERSION 3.16) + +set(CMAKE_CXX_STANDARD 11) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS OFF) + +file(GLOB SOURCES "src/*.c") +file(GLOB HEADERS "src/*.h") + +add_library(lv_screenshot STATIC) + +target_sources(lv_screenshot + PRIVATE ${SOURCES} + PUBLIC ${HEADERS} +) + +target_include_directories(lv_screenshot + PRIVATE private + PUBLIC src +) + +if (DEFINED ENV{ESP_IDF_VERSION}) + target_link_libraries(lv_screenshot + PUBLIC idf::lvgl + ) +else() + target_link_libraries(lv_screenshot + PUBLIC lvgl + ) +endif() \ No newline at end of file diff --git a/libs/lv_screenshot/LICENSE-original b/libs/lv_screenshot/LICENSE-original new file mode 100644 index 00000000..e3a7b109 --- /dev/null +++ b/libs/lv_screenshot/LICENSE-original @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022 深圳百问网科技有限公司(www.100ask.net) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/libs/lv_screenshot/README.md b/libs/lv_screenshot/README.md new file mode 100644 index 00000000..05770d3f --- /dev/null +++ b/libs/lv_screenshot/README.md @@ -0,0 +1,12 @@ +## lv_screenshot + +This library is adapted from the lv_100ask_screenshot library from 100ask on [GitHub](https://github.com/100askTeam/lv_lib_100ask). + +The original license is available [here](LICENSE-original). + +## Features + +- Save LVGL screen objects (full screen) as image files: lv_scr_act(),layer_sys(),layer_top() +- Capture and save the specified LVGL object and its children as an image file +- Supported save as: BMP, PNG, JPG +- more todo... diff --git a/libs/lv_screenshot/private/save_bmp.h b/libs/lv_screenshot/private/save_bmp.h new file mode 100644 index 00000000..a4e5a133 --- /dev/null +++ b/libs/lv_screenshot/private/save_bmp.h @@ -0,0 +1,14 @@ +#pragma once + +#include "lvgl.h" +#include + +#ifdef __cplusplus +extern "C" { +#endif + +bool lve_screenshot_save_bmp_file(const uint8_t* image, uint32_t w, uint32_t h, uint32_t bpp, const char* filename); + +#ifdef __cplusplus +} +#endif diff --git a/libs/lv_screenshot/private/save_png.h b/libs/lv_screenshot/private/save_png.h new file mode 100644 index 00000000..fa7087bc --- /dev/null +++ b/libs/lv_screenshot/private/save_png.h @@ -0,0 +1,14 @@ +#pragma once + +#include "lvgl.h" +#include + +#ifdef __cplusplus +extern "C" { +#endif + +bool lv_screenshot_save_png_file(const uint8_t* image, uint32_t w, uint32_t h, uint32_t bpp, const char* filename); + +#ifdef __cplusplus +} +#endif diff --git a/libs/lv_screenshot/src/lv_screenshot.c b/libs/lv_screenshot/src/lv_screenshot.c new file mode 100644 index 00000000..6240fe24 --- /dev/null +++ b/libs/lv_screenshot/src/lv_screenshot.c @@ -0,0 +1,67 @@ +#include "lv_screenshot.h" + +#include "save_bmp.h" +#include "save_png.h" + +static void data_pre_processing(lv_img_dsc_t* snapshot, uint16_t bpp, lv_100ask_screenshot_sv_t screenshot_sv); + +bool lv_screenshot_create(lv_obj_t* obj, lv_img_cf_t cf, lv_100ask_screenshot_sv_t screenshot_sv, const char* filename) { + lv_img_dsc_t* snapshot = lv_snapshot_take(obj, cf); + + if (snapshot) { + data_pre_processing(snapshot, LV_COLOR_DEPTH, screenshot_sv); + + 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); + } + } else if (screenshot_sv == LV_100ASK_SCREENSHOT_SV_BMP) { + if (LV_COLOR_DEPTH == 16) { + lve_screenshot_save_bmp_file(snapshot->data, snapshot->header.w, snapshot->header.h, 24, filename); + } else if (LV_COLOR_DEPTH == 32) { + lve_screenshot_save_bmp_file(snapshot->data, snapshot->header.w, snapshot->header.h, 32, filename); + } + } + + lv_snapshot_free(snapshot); + return true; + } + + return false; +} + +static void data_pre_processing(lv_img_dsc_t* snapshot, uint16_t bpp, lv_100ask_screenshot_sv_t screenshot_sv) { + if (bpp == 16) { + uint16_t rgb565_data = 0; + uint32_t count = 0; + for (int w = 0; w < snapshot->header.w; w++) { + for (int h = 0; h < snapshot->header.h; h++) { + rgb565_data = (uint16_t)((*(uint8_t*)(snapshot->data + count + 1) << 8) | *(uint8_t*)(snapshot->data + count)); + if (screenshot_sv == LV_100ASK_SCREENSHOT_SV_PNG) { + *(uint8_t*)(snapshot->data + count) = (uint8_t)(((rgb565_data) >> 11) << 3); + *(uint8_t*)(snapshot->data + count + 1) = (uint8_t)(((rgb565_data) >> 5) << 2); + *(uint8_t*)(snapshot->data + count + 2) = (uint8_t)(((rgb565_data) >> 0) << 3); + } else if (screenshot_sv == LV_100ASK_SCREENSHOT_SV_BMP) { + *(uint8_t*)(snapshot->data + count) = (uint8_t)(((rgb565_data) >> 0) << 3); + *(uint8_t*)(snapshot->data + count + 1) = (uint8_t)(((rgb565_data) >> 5) << 2); + *(uint8_t*)(snapshot->data + count + 2) = (uint8_t)(((rgb565_data) >> 11) << 3); + } + + count += 3; + } + } + } else if ((screenshot_sv == LV_100ASK_SCREENSHOT_SV_PNG) && (bpp == 32)) { + uint8_t tmp_data = 0; + uint32_t count = 0; + 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; + } + } + } +} diff --git a/libs/lv_screenshot/src/lv_screenshot.h b/libs/lv_screenshot/src/lv_screenshot.h new file mode 100644 index 00000000..489343f3 --- /dev/null +++ b/libs/lv_screenshot/src/lv_screenshot.h @@ -0,0 +1,21 @@ +#pragma once + +#include "lvgl.h" +#include + + +#ifdef __cplusplus +extern "C" { +#endif + +typedef enum { + LV_100ASK_SCREENSHOT_SV_BMP = 0, + LV_100ASK_SCREENSHOT_SV_PNG = 1, + LV_100ASK_SCREENSHOT_SV_LAST +} lv_100ask_screenshot_sv_t; + +bool lv_screenshot_create(lv_obj_t* obj, lv_img_cf_t cf, lv_100ask_screenshot_sv_t screenshot_sv, const char* filename); + +#ifdef __cplusplus +} /*extern "C"*/ +#endif diff --git a/libs/lv_screenshot/src/save_bmp.c b/libs/lv_screenshot/src/save_bmp.c new file mode 100644 index 00000000..29224417 --- /dev/null +++ b/libs/lv_screenshot/src/save_bmp.c @@ -0,0 +1,93 @@ +#include "save_bmp.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; +} diff --git a/libs/lv_screenshot/src/save_png.c b/libs/lv_screenshot/src/save_png.c new file mode 100644 index 00000000..e997efc5 --- /dev/null +++ b/libs/lv_screenshot/src/save_png.c @@ -0,0 +1,11 @@ +#include "save_png.h" +#include "src/extra/libs/png/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); + } else if (bpp == 24) { + return lodepng_encode24_file(filename, image, w, h); + } + return false; +} diff --git a/sdkconfig.board.lilygo_tdeck b/sdkconfig.board.lilygo_tdeck index 999d0632..dcb845f3 100644 --- a/sdkconfig.board.lilygo_tdeck +++ b/sdkconfig.board.lilygo_tdeck @@ -24,6 +24,7 @@ CONFIG_FLASHMODE_QIO=y CONFIG_ESP32S3_SPIRAM_SUPPORT=y CONFIG_SPIRAM_MODE_OCT=y CONFIG_SPIRAM_SPEED_80M=y +CONFIG_SPIRAM_TRY_ALLOCATE_WIFI_LWIP=y # LVGL CONFIG_LV_COLOR_16_SWAP=y CONFIG_LV_DISP_DEF_REFR_PERIOD=17 diff --git a/tactility/CMakeLists.txt b/tactility/CMakeLists.txt index cebe4538..a1a6216c 100644 --- a/tactility/CMakeLists.txt +++ b/tactility/CMakeLists.txt @@ -25,6 +25,7 @@ if (DEFINED ENV{ESP_IDF_VERSION}) PUBLIC idf::spiffs PUBLIC idf::nvs_flash PUBLIC idf::newlib # for scandir() and related + PUBLIC lv_screenshot ) else() add_definitions(-D_Nullable=) @@ -33,6 +34,7 @@ else() PUBLIC tactility-core PUBLIC lvgl PUBLIC freertos-kernel + PUBLIC lv_screenshot ) endif() diff --git a/tactility/src/apps/screenshot/screenshot.c b/tactility/src/apps/screenshot/screenshot.c new file mode 100644 index 00000000..7f6d87ef --- /dev/null +++ b/tactility/src/apps/screenshot/screenshot.c @@ -0,0 +1,29 @@ +#include "tactility_core.h" +#include "ui/toolbar.h" +#include "screenshot_ui.h" + +static void on_show(App app, lv_obj_t* parent) { + ScreenshotUi* ui = tt_app_get_data(app); + create_screenshot_ui(app, ui, parent); +} + +static void on_start(App app) { + ScreenshotUi* ui = malloc(sizeof(ScreenshotUi)); + tt_app_set_data(app, ui); +} + +static void on_stop(App app) { + ScreenshotUi* ui = tt_app_get_data(app); + free(ui); +} + +const AppManifest screenshot_app = { + .id = "screenshot", + .name = "Screenshot", + .icon = LV_SYMBOL_IMAGE, + .type = AppTypeSystem, + .on_start = &on_start, + .on_stop = &on_stop, + .on_show = &on_show, + .on_hide = NULL +}; diff --git a/tactility/src/apps/screenshot/screenshot_ui.c b/tactility/src/apps/screenshot/screenshot_ui.c new file mode 100644 index 00000000..8dfe3dc3 --- /dev/null +++ b/tactility/src/apps/screenshot/screenshot_ui.c @@ -0,0 +1,169 @@ +#include "screenshot_ui.h" + +#include "sdcard.h" +#include "services/screenshot/screenshot.h" +#include "tactility_core.h" +#include "ui/toolbar.h" + +#define TAG "screenshot_ui" + +static void update_mode(ScreenshotUi* ui) { + lv_obj_t* label = ui->start_stop_button_label; + if (tt_screenshot_is_started()) { + lv_label_set_text(label, "Stop"); + } else { + lv_label_set_text(label, "Start"); + } + + int selected = lv_dropdown_get_selected(ui->mode_dropdown); + if (selected == 0) { // Timer + lv_obj_clear_flag(ui->timer_wrapper, LV_OBJ_FLAG_HIDDEN); + } else { + lv_obj_add_flag(ui->timer_wrapper, LV_OBJ_FLAG_HIDDEN); + } +} + +static void on_mode_set(lv_event_t* event) { + ScreenshotUi* ui = (ScreenshotUi*)event->user_data; + update_mode(ui); +} + +static void on_start_pressed(TT_UNUSED lv_event_t* event) { + ScreenshotUi* ui = event->user_data; + + if (tt_screenshot_is_started()) { + TT_LOG_I(TAG, "Stop screenshot"); + tt_screenshot_stop(); + } else { + int selected = lv_dropdown_get_selected(ui->mode_dropdown); + const char* path = lv_textarea_get_text(ui->path_textarea); + if (selected == 0) { + TT_LOG_I(TAG, "Start timed screenshots"); + const char* delay_text = lv_textarea_get_text(ui->delay_textarea); + int delay = atoi(delay_text); + if (delay > 0) { + tt_screenshot_start_timed(path, delay, 1); + } else { + TT_LOG_W(TAG, "Ignored screenshot start because delay was 0"); + } + } else { + TT_LOG_I(TAG, "Start app screenshots"); + tt_screenshot_start_apps(path); + } + } + + update_mode(ui); +} + +static void create_mode_setting_ui(ScreenshotUi* ui, lv_obj_t* parent) { + lv_obj_t* 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); + 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, ui); + ui->mode_dropdown = mode_dropdown; + ScreenshotMode mode = tt_screenshot_get_mode(); + if (mode == ScreenshotModeApps) { + lv_dropdown_set_selected(mode_dropdown, 1); + } + + lv_obj_t* button = lv_btn_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, ui); +} + +static void create_path_ui(ScreenshotUi* ui, lv_obj_t* parent) { + lv_obj_t* 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); + 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); + 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; + if (tt_get_platform() == PlatformEsp) { + if (tt_sdcard_get_state() == SdcardStateMounted) { + lv_textarea_set_text(path_textarea, "A:/sdcard"); + } else { + lv_textarea_set_text(path_textarea, "Error: no SD card"); + } + } else { // PC + lv_textarea_set_text(path_textarea, "A:"); + } +} + +static void create_timer_settings_ui(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; + + lv_obj_t* delay_wrapper = lv_obj_create(timer_wrapper); + 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); + 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); + 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; + + lv_obj_t* 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); + lv_obj_align(delay_unit_label, LV_ALIGN_LEFT_MID, 0, 0); + lv_label_set_text(delay_unit_label, "seconds"); +} + +void create_screenshot_ui(App app, ScreenshotUi* ui, lv_obj_t* parent) { + lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN); + lv_obj_t* toolbar = tt_toolbar_create_for_app(parent, app); + lv_obj_align(toolbar, LV_ALIGN_TOP_MID, 0, 0); + + lv_obj_t* 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); + + update_mode(ui); +} diff --git a/tactility/src/apps/screenshot/screenshot_ui.h b/tactility/src/apps/screenshot/screenshot_ui.h new file mode 100644 index 00000000..1df9fbd8 --- /dev/null +++ b/tactility/src/apps/screenshot/screenshot_ui.h @@ -0,0 +1,22 @@ +#pragma once + +#include "app.h" +#include "lvgl.h" + +#ifdef __cplusplus +extern "C" { +#endif + +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; + +void create_screenshot_ui(App app, ScreenshotUi* ui, lv_obj_t* parent); + +#ifdef __cplusplus +} +#endif \ No newline at end of file diff --git a/tactility/src/service_registry.c b/tactility/src/service_registry.c index 2f48890a..6ece86ab 100644 --- a/tactility/src/service_registry.c +++ b/tactility/src/service_registry.c @@ -116,6 +116,14 @@ bool tt_service_registry_start(const char* service_id) { return true; } +Service _Nullable tt_service_find(const char* service_id) { + service_registry_instance_lock(); + const ServiceData** _Nullable service_ptr = ServiceInstanceDict_get(service_instance_dict, service_id); + const ServiceData* service = service_ptr ? *service_ptr : NULL; + service_registry_instance_unlock(); + return (Service)service; +} + bool tt_service_registry_stop(const char* service_id) { TT_LOG_I(TAG, "stopping %s", service_id); ServiceData* service = service_registry_find_instance_by_id(service_id); diff --git a/tactility/src/service_registry.h b/tactility/src/service_registry.h index 7942e242..d6f03ca6 100644 --- a/tactility/src/service_registry.h +++ b/tactility/src/service_registry.h @@ -21,6 +21,8 @@ void tt_service_registry_for_each_manifest(ServiceManifestCallback callback, voi bool tt_service_registry_start(const char* service_id); bool tt_service_registry_stop(const char* service_id); +Service _Nullable tt_service_find(const char* service_id); + #ifdef __cplusplus } #endif diff --git a/tactility/src/services/screenshot/screenshot.c b/tactility/src/services/screenshot/screenshot.c new file mode 100644 index 00000000..7b5fe2c4 --- /dev/null +++ b/tactility/src/services/screenshot/screenshot.c @@ -0,0 +1,133 @@ +#include "screenshot.h" + +#include "mutex.h" +#include "screenshot_task.h" +#include "service.h" +#include "service_registry.h" +#include "tactility_core.h" + +#define TAG "sdcard_service" + +typedef struct { + Mutex* mutex; + ScreenshotTask* task; + ScreenshotMode mode; +} ServiceData; + +static ServiceData* service_data_alloc() { + ServiceData* data = malloc(sizeof(ServiceData)); + *data = (ServiceData) { + .mutex = tt_mutex_alloc(MutexTypeNormal), + .task = NULL + }; + return data; +} + +static void service_data_free(ServiceData* data) { + tt_mutex_free(data->mutex); +} + +static void service_data_lock(ServiceData* data) { + tt_check(tt_mutex_acquire(data->mutex, TtWaitForever) == TtStatusOk); +} + +static void service_data_unlock(ServiceData* data) { + tt_check(tt_mutex_release(data->mutex) == TtStatusOk); +} + +static void on_start(Service service) { + ServiceData* data = service_data_alloc(); + tt_service_set_data(service, data); +} + +static void on_stop(Service service) { + ServiceData* data = tt_service_get_data(service); + if (data->task) { + screenshot_task_free(data->task); + data->task = NULL; + } + tt_mutex_free(data->mutex); + service_data_free(data); +} + +const ServiceManifest screenshot_service = { + .id = "screenshot", + .on_start = &on_start, + .on_stop = &on_stop +}; + +void tt_screenshot_start_apps(const char* path) { + Service _Nullable service = tt_service_find(screenshot_service.id); + if (service == NULL) { + TT_LOG_E(TAG, "Service not found"); + return; + } + + ServiceData* data = tt_service_get_data(service); + service_data_lock(data); + if (data->task == NULL) { + data->task = screenshot_task_alloc(); + data->mode = ScreenshotModeApps; + screenshot_task_start_apps(data->task, path); + } else { + TT_LOG_E(TAG, "Screenshot task already running"); + } + service_data_unlock(data); +} + +void tt_screenshot_start_timed(const char* path, uint8_t delay_in_seconds, uint8_t amount) { + Service _Nullable service = tt_service_find(screenshot_service.id); + if (service == NULL) { + TT_LOG_E(TAG, "Service not found"); + return; + } + + ServiceData* data = tt_service_get_data(service); + service_data_lock(data); + if (data->task == NULL) { + data->task = screenshot_task_alloc(); + data->mode = ScreenshotModeTimed; + screenshot_task_start_timed(data->task, path, delay_in_seconds, amount); + } else { + TT_LOG_E(TAG, "Screenshot task already running"); + } + service_data_unlock(data); +} + +void tt_screenshot_stop() { + Service _Nullable service = tt_service_find(screenshot_service.id); + if (service == NULL) { + TT_LOG_E(TAG, "Service not found"); + return; + } + + ServiceData* data = tt_service_get_data(service); + service_data_lock(data); + if (data->task != NULL) { + screenshot_task_stop(data->task); + screenshot_task_free(data->task); + data->task = NULL; + data->mode = ScreenshotModeNone; + } else { + TT_LOG_E(TAG, "Screenshot task not running"); + } + service_data_unlock(data); +} + +ScreenshotMode tt_screenshot_get_mode() { + Service _Nullable service = tt_service_find(screenshot_service.id); + if (service == NULL) { + TT_LOG_E(TAG, "Service not found"); + return ScreenshotModeNone; + } else { + ServiceData* data = tt_service_get_data(service); + service_data_lock(data); + ScreenshotMode mode = data->mode; + service_data_unlock(data); + return mode; + } +} + +bool tt_screenshot_is_started() { + return tt_screenshot_get_mode() != ScreenshotModeNone; +} diff --git a/tactility/src/services/screenshot/screenshot.h b/tactility/src/services/screenshot/screenshot.h new file mode 100644 index 00000000..a6e967a7 --- /dev/null +++ b/tactility/src/services/screenshot/screenshot.h @@ -0,0 +1,36 @@ +#pragma once + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +typedef enum { + ScreenshotModeNone, + ScreenshotModeTimed, + ScreenshotModeApps +} ScreenshotMode; + +/** @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 tt_screenshot_start_timed(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 tt_screenshot_start_apps(const char* path); + +void tt_screenshot_stop(); + +ScreenshotMode tt_screenshot_get_mode(); + +bool tt_screenshot_is_started(); + +#ifdef __cplusplus +} +#endif \ No newline at end of file diff --git a/tactility/src/services/screenshot/screenshot_task.c b/tactility/src/services/screenshot/screenshot_task.c new file mode 100644 index 00000000..b14cef09 --- /dev/null +++ b/tactility/src/services/screenshot/screenshot_task.c @@ -0,0 +1,183 @@ +#include "screenshot_task.h" +#include "lv_screenshot.h" + +#include "app.h" +#include "mutex.h" +#include "services/loader/loader.h" +#include "tactility_core.h" +#include "thread.h" +#include "ui/lvgl_sync.h" + +#define TAG "screenshot_task" + +#define TASK_WORK_TYPE_DELAY 1 +#define TASK_WORK_TYPE_APPS 2 + +#define SCREENSHOT_PATH_LIMIT 128 + +typedef struct { + int type; + uint8_t delay_in_seconds; + uint8_t amount; + char path[SCREENSHOT_PATH_LIMIT]; +} ScreenshotTaskWork; + +typedef struct { + Thread* thread; + Mutex* mutex; + bool interrupted; + ScreenshotTaskWork work; +} ScreenshotTaskData; + +static void screenshot_task_lock(ScreenshotTaskData* data) { + tt_check(tt_mutex_acquire(data->mutex, TtWaitForever) == TtStatusOk); +} + +static void screenshot_task_unlock(ScreenshotTaskData* data) { + tt_check(tt_mutex_release(data->mutex) == TtStatusOk); +} + +ScreenshotTask* screenshot_task_alloc() { + ScreenshotTaskData* data = malloc(sizeof(ScreenshotTaskData)); + *data = (ScreenshotTaskData) { + .thread = NULL, + .mutex = tt_mutex_alloc(MutexTypeRecursive), + .interrupted = false + }; + return data; +} + +void screenshot_task_free(ScreenshotTask* task) { + ScreenshotTaskData* data = (ScreenshotTaskData*)task; + if (data->thread) { + screenshot_task_stop(data); + } +} + +static bool is_interrupted(ScreenshotTaskData* data) { + screenshot_task_lock(data); + bool interrupted = data->interrupted; + screenshot_task_unlock(data); + return interrupted; +} + +static int32_t screenshot_task(void* context) { + ScreenshotTaskData* data = (ScreenshotTaskData*)context; + + bool interrupted = false; + uint8_t screenshots_taken = 0; + const char* last_app_id = NULL; + + 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){ + tt_delay_ms(100); + } + + if (is_interrupted(data)) { + break; + } + + screenshots_taken++; + char filename[SCREENSHOT_PATH_LIMIT + 32]; + sprintf(filename, "%s/screenshot-%d.png", data->work.path, screenshots_taken); + tt_lvgl_lock(TtWaitForever); + if (lv_screenshot_create(lv_scr_act(), LV_IMG_CF_TRUE_COLOR, 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); + } + tt_lvgl_unlock(); + + if (data->work.amount > 0 && screenshots_taken >= data->work.amount) { + break; // Interrupted loop + } + } else if (data->work.type == TASK_WORK_TYPE_APPS) { + App _Nullable app = loader_get_current_app(); + if (app) { + const AppManifest* manifest = tt_app_get_manifest(app); + if (manifest->id != last_app_id) { + tt_delay_ms(100); + last_app_id = manifest->id; + + char filename[SCREENSHOT_PATH_LIMIT + 32]; + sprintf(filename, "%s/screenshot-%s.png", data->work.path, manifest->id); + tt_lvgl_lock(TtWaitForever); + if (lv_screenshot_create(lv_scr_act(), LV_IMG_CF_TRUE_COLOR, 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); + } + tt_lvgl_unlock(); + } + } + tt_delay_ms(250); + } + } + + return 0; +} + +static void screenshot_task_start(ScreenshotTaskData* data) { + screenshot_task_lock(data); + tt_check(data->thread == NULL); + data->thread = tt_thread_alloc_ex( + "screenshot", + 8192, + &screenshot_task, + data + ); + tt_thread_start(data->thread); + screenshot_task_unlock(data); +} + +void screenshot_task_start_apps(ScreenshotTask* task, const char* path) { + tt_check(strlen(path) < (SCREENSHOT_PATH_LIMIT - 1)); + ScreenshotTaskData* data = (ScreenshotTaskData*)task; + screenshot_task_lock(data); + if (data->thread == NULL) { + data->interrupted = false; + data->work.type = TASK_WORK_TYPE_APPS; + strcpy(data->work.path, path); + screenshot_task_start(data); + } else { + TT_LOG_E(TAG, "Task was already running"); + } + screenshot_task_unlock(data); +} + +void screenshot_task_start_timed(ScreenshotTask* task, const char* path, uint8_t delay_in_seconds, uint8_t amount) { + tt_check(strlen(path) < (SCREENSHOT_PATH_LIMIT - 1)); + ScreenshotTaskData* data = (ScreenshotTaskData*)task; + screenshot_task_lock(data); + if (data->thread == NULL) { + 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); + screenshot_task_start(data); + } else { + TT_LOG_E(TAG, "Task was already running"); + } + screenshot_task_unlock(data); +} + +void screenshot_task_stop(ScreenshotTask* task) { + ScreenshotTaskData* data = (ScreenshotTaskData*)task; + if (data->thread != NULL) { + screenshot_task_lock(data); + data->interrupted = true; + screenshot_task_unlock(data); + + tt_thread_join(data->thread); + + screenshot_task_lock(data); + tt_thread_free(data->thread); + data->thread = NULL; + screenshot_task_unlock(data); + } +} \ No newline at end of file diff --git a/tactility/src/services/screenshot/screenshot_task.h b/tactility/src/services/screenshot/screenshot_task.h new file mode 100644 index 00000000..606f1f19 --- /dev/null +++ b/tactility/src/services/screenshot/screenshot_task.h @@ -0,0 +1,36 @@ +#pragma once + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +typedef void ScreenshotTask; + +ScreenshotTask* screenshot_task_alloc(); + +void screenshot_task_free(ScreenshotTask* task); + +/** @brief Start taking screenshots after a certain delay + * @param task the screenshot task + * @param path the path to store the screenshots at + * @param delay_in_seconds the delay before starting (and between successive screenshots) + * @param amount 0 = indefinite, >0 for a specific + */ +void screenshot_task_start_timed(ScreenshotTask* task, 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 screenshot_task_start_apps(ScreenshotTask* task, const char* path); + +/** @brief Stop taking screenshots + * @param task the screenshot task + */ +void screenshot_task_stop(ScreenshotTask* task); + +#ifdef __cplusplus +} +#endif \ No newline at end of file diff --git a/tactility/src/tactility.c b/tactility/src/tactility.c index ac4d5f4b..2f73d012 100644 --- a/tactility/src/tactility.c +++ b/tactility/src/tactility.c @@ -13,11 +13,15 @@ static const Config* config_instance = NULL; extern const ServiceManifest gui_service; extern const ServiceManifest loader_service; +extern const ServiceManifest screenshot_service; extern const ServiceManifest sdcard_service; static const ServiceManifest* const system_services[] = { &gui_service, &loader_service, // depends on gui service +#ifndef ESP_PLATFORM // Screenshots don't work yet on ESP32 + &screenshot_service, +#endif &sdcard_service }; @@ -28,6 +32,7 @@ static const ServiceManifest* const system_services[] = { extern const AppManifest desktop_app; extern const AppManifest display_app; extern const AppManifest files_app; +extern const AppManifest screenshot_app; extern const AppManifest settings_app; extern const AppManifest system_info_app; @@ -35,6 +40,9 @@ static const AppManifest* const system_apps[] = { &desktop_app, &display_app, &files_app, +#ifndef ESP_PLATFORM // Screenshots don't work yet on ESP32 + &screenshot_app, +#endif &settings_app, &system_info_app };