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
@ -35,6 +35,7 @@ endif()
|
|||||||
project(tactility-root)
|
project(tactility-root)
|
||||||
|
|
||||||
add_subdirectory(libs/mlib)
|
add_subdirectory(libs/mlib)
|
||||||
|
add_subdirectory(libs/lv_screenshot)
|
||||||
add_subdirectory(tactility)
|
add_subdirectory(tactility)
|
||||||
add_subdirectory(tactility-core)
|
add_subdirectory(tactility-core)
|
||||||
|
|
||||||
|
|||||||
14
README.md
@ -5,7 +5,17 @@ It provides an application framework that is based on code from the [Flipper Zer
|
|||||||
|
|
||||||
**Status: Alpha**
|
**Status: Alpha**
|
||||||
|
|
||||||

|
Tactility features a desktop that can launch apps:
|
||||||
|
|
||||||
|
  
|
||||||
|
|
||||||
|
Through the Settings app you can connect to Wi-Fi or change the display settings:
|
||||||
|
|
||||||
|
 
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
Noteworthy features:
|
Noteworthy features:
|
||||||
- Touch UI capabilities (via LVGL) with support for input devices such as on-device trackball or keyboard.
|
- 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 = {
|
|||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
## Supported Hardware
|
## Supported Hardware
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +1,9 @@
|
|||||||
#include "hello_world/hello_world.h"
|
#include "hello_world/hello_world.h"
|
||||||
#include "tactility.h"
|
#include "tactility.h"
|
||||||
|
#include "assets.h"
|
||||||
|
|
||||||
#include "FreeRTOS.h"
|
#include "FreeRTOS.h"
|
||||||
|
#include "ui/statusbar.h"
|
||||||
|
|
||||||
#define TAG "main"
|
#define TAG "main"
|
||||||
|
|
||||||
@ -18,4 +20,9 @@ void app_main() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
tt_init(&config);
|
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);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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))
|
- 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?
|
- 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
|
- 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
|
# 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.
|
- 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.
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 5.2 KiB |
BIN
docs/pics/screenshot-desktop.png
Normal file
|
After Width: | Height: | Size: 3.9 KiB |
BIN
docs/pics/screenshot-display.png
Normal file
|
After Width: | Height: | Size: 3.4 KiB |
BIN
docs/pics/screenshot-files.png
Normal file
|
After Width: | Height: | Size: 2.9 KiB |
BIN
docs/pics/screenshot-helloworld.png
Normal file
|
After Width: | Height: | Size: 2.2 KiB |
BIN
docs/pics/screenshot-settings.png
Normal file
|
After Width: | Height: | Size: 3.1 KiB |
BIN
docs/pics/screenshot-systeminfo.png
Normal file
|
After Width: | Height: | Size: 4.0 KiB |
BIN
docs/pics/tactility-devices.jpg
Normal file
|
After Width: | Height: | Size: 100 KiB |
|
Before Width: | Height: | Size: 84 KiB |
30
libs/lv_screenshot/CMakeLists.txt
Normal 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()
|
||||||
21
libs/lv_screenshot/LICENSE-original
Normal 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.
|
||||||
12
libs/lv_screenshot/README.md
Normal 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...
|
||||||
14
libs/lv_screenshot/private/save_bmp.h
Normal 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
|
||||||
14
libs/lv_screenshot/private/save_png.h
Normal 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
|
||||||
67
libs/lv_screenshot/src/lv_screenshot.c
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
21
libs/lv_screenshot/src/lv_screenshot.h
Normal 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
|
||||||
93
libs/lv_screenshot/src/save_bmp.c
Normal 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;
|
||||||
|
}
|
||||||
11
libs/lv_screenshot/src/save_png.c
Normal 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;
|
||||||
|
}
|
||||||
@ -24,6 +24,7 @@ CONFIG_FLASHMODE_QIO=y
|
|||||||
CONFIG_ESP32S3_SPIRAM_SUPPORT=y
|
CONFIG_ESP32S3_SPIRAM_SUPPORT=y
|
||||||
CONFIG_SPIRAM_MODE_OCT=y
|
CONFIG_SPIRAM_MODE_OCT=y
|
||||||
CONFIG_SPIRAM_SPEED_80M=y
|
CONFIG_SPIRAM_SPEED_80M=y
|
||||||
|
CONFIG_SPIRAM_TRY_ALLOCATE_WIFI_LWIP=y
|
||||||
# LVGL
|
# LVGL
|
||||||
CONFIG_LV_COLOR_16_SWAP=y
|
CONFIG_LV_COLOR_16_SWAP=y
|
||||||
CONFIG_LV_DISP_DEF_REFR_PERIOD=17
|
CONFIG_LV_DISP_DEF_REFR_PERIOD=17
|
||||||
|
|||||||
@ -25,6 +25,7 @@ if (DEFINED ENV{ESP_IDF_VERSION})
|
|||||||
PUBLIC idf::spiffs
|
PUBLIC idf::spiffs
|
||||||
PUBLIC idf::nvs_flash
|
PUBLIC idf::nvs_flash
|
||||||
PUBLIC idf::newlib # for scandir() and related
|
PUBLIC idf::newlib # for scandir() and related
|
||||||
|
PUBLIC lv_screenshot
|
||||||
)
|
)
|
||||||
else()
|
else()
|
||||||
add_definitions(-D_Nullable=)
|
add_definitions(-D_Nullable=)
|
||||||
@ -33,6 +34,7 @@ else()
|
|||||||
PUBLIC tactility-core
|
PUBLIC tactility-core
|
||||||
PUBLIC lvgl
|
PUBLIC lvgl
|
||||||
PUBLIC freertos-kernel
|
PUBLIC freertos-kernel
|
||||||
|
PUBLIC lv_screenshot
|
||||||
)
|
)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
|
|||||||
29
tactility/src/apps/screenshot/screenshot.c
Normal 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
|
||||||
|
};
|
||||||
169
tactility/src/apps/screenshot/screenshot_ui.c
Normal 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);
|
||||||
|
}
|
||||||
22
tactility/src/apps/screenshot/screenshot_ui.h
Normal 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
|
||||||
@ -116,6 +116,14 @@ bool tt_service_registry_start(const char* service_id) {
|
|||||||
return true;
|
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) {
|
bool tt_service_registry_stop(const char* service_id) {
|
||||||
TT_LOG_I(TAG, "stopping %s", service_id);
|
TT_LOG_I(TAG, "stopping %s", service_id);
|
||||||
ServiceData* service = service_registry_find_instance_by_id(service_id);
|
ServiceData* service = service_registry_find_instance_by_id(service_id);
|
||||||
|
|||||||
@ -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_start(const char* service_id);
|
||||||
bool tt_service_registry_stop(const char* service_id);
|
bool tt_service_registry_stop(const char* service_id);
|
||||||
|
|
||||||
|
Service _Nullable tt_service_find(const char* service_id);
|
||||||
|
|
||||||
#ifdef __cplusplus
|
#ifdef __cplusplus
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
133
tactility/src/services/screenshot/screenshot.c
Normal 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;
|
||||||
|
}
|
||||||
36
tactility/src/services/screenshot/screenshot.h
Normal 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
|
||||||
183
tactility/src/services/screenshot/screenshot_task.c
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
36
tactility/src/services/screenshot/screenshot_task.h
Normal 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
|
||||||
@ -13,11 +13,15 @@ static const Config* config_instance = NULL;
|
|||||||
|
|
||||||
extern const ServiceManifest gui_service;
|
extern const ServiceManifest gui_service;
|
||||||
extern const ServiceManifest loader_service;
|
extern const ServiceManifest loader_service;
|
||||||
|
extern const ServiceManifest screenshot_service;
|
||||||
extern const ServiceManifest sdcard_service;
|
extern const ServiceManifest sdcard_service;
|
||||||
|
|
||||||
static const ServiceManifest* const system_services[] = {
|
static const ServiceManifest* const system_services[] = {
|
||||||
&gui_service,
|
&gui_service,
|
||||||
&loader_service, // depends on gui service
|
&loader_service, // depends on gui service
|
||||||
|
#ifndef ESP_PLATFORM // Screenshots don't work yet on ESP32
|
||||||
|
&screenshot_service,
|
||||||
|
#endif
|
||||||
&sdcard_service
|
&sdcard_service
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -28,6 +32,7 @@ static const ServiceManifest* const system_services[] = {
|
|||||||
extern const AppManifest desktop_app;
|
extern const AppManifest desktop_app;
|
||||||
extern const AppManifest display_app;
|
extern const AppManifest display_app;
|
||||||
extern const AppManifest files_app;
|
extern const AppManifest files_app;
|
||||||
|
extern const AppManifest screenshot_app;
|
||||||
extern const AppManifest settings_app;
|
extern const AppManifest settings_app;
|
||||||
extern const AppManifest system_info_app;
|
extern const AppManifest system_info_app;
|
||||||
|
|
||||||
@ -35,6 +40,9 @@ static const AppManifest* const system_apps[] = {
|
|||||||
&desktop_app,
|
&desktop_app,
|
||||||
&display_app,
|
&display_app,
|
||||||
&files_app,
|
&files_app,
|
||||||
|
#ifndef ESP_PLATFORM // Screenshots don't work yet on ESP32
|
||||||
|
&screenshot_app,
|
||||||
|
#endif
|
||||||
&settings_app,
|
&settings_app,
|
||||||
&system_info_app
|
&system_info_app
|
||||||
};
|
};
|
||||||
|
|||||||