Screenshot app & service (#42)

- Added screenshot app & service (PC-only for now)
- Updated docs with screenshots and new device photo
- Add fake statusbar icons for PC/sim build
- added `lv_screenshot` library based on `lv_100ask_screenshot` from https://github.com/100askTeam/lv_lib_100ask
- T-Deck WiFi is now allocated into SPI RAM
- Created `tt_service_find()` to find services by their id
This commit is contained in:
Ken Van Hoeylandt 2024-02-11 22:40:53 +01:00 committed by GitHub
parent 0bdc4bd32f
commit 3250943345
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
34 changed files with 934 additions and 2 deletions

View File

@ -35,6 +35,7 @@ endif()
project(tactility-root)
add_subdirectory(libs/mlib)
add_subdirectory(libs/lv_screenshot)
add_subdirectory(tactility)
add_subdirectory(tactility-core)

View File

@ -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

View File

@ -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);
}

View File

@ -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.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 84 KiB

View File

@ -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()

View File

@ -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.

View File

@ -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...

View File

@ -0,0 +1,14 @@
#pragma once
#include "lvgl.h"
#include <stdbool.h>
#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

View File

@ -0,0 +1,14 @@
#pragma once
#include "lvgl.h"
#include <stdbool.h>
#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

View File

@ -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;
}
}
}
}

View File

@ -0,0 +1,21 @@
#pragma once
#include "lvgl.h"
#include <stdbool.h>
#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

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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

View File

@ -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()

View File

@ -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
};

View File

@ -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);
}

View File

@ -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

View File

@ -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);

View File

@ -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

View File

@ -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;
}

View File

@ -0,0 +1,36 @@
#pragma once
#include <stdbool.h>
#include <stdint.h>
#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

View File

@ -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);
}
}

View File

@ -0,0 +1,36 @@
#pragma once
#include <stdint.h>
#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

View File

@ -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
};