App hub and more (#383)

- Added `AppHub` app
- Added `AppHubDetails` app
- Added `cJSON` dependency
- Renamed `AppSim` module to `FirmwareSim`
- Added extra `tt::app::alertdialg::start()`
- Renamed `addApp()`, `removeApp()`, `findAppById()` and `getApps()` to `addAppManifest()`, `removeAppManifest()`, `findAppManifestById()` and `getAppManifests()`
- Added `tt::lvgl::toolbar_clear_actions()`
- Added `tt::network::EspHttpClient` as a thread-safe wrapper around `esp_http_client`
- Added `tt::network::http::download()` to download files
- Added `tt::network::ntp::isSynced()`
- When time is synced, the timestamp is stored in NVS flash. On boot, it is restored. This helps SSL connections when doing a quick reset: when WiFi reconnects, the user doesn't have to wait for NTP sync before SSL works.
- Added `tt::json::Reader` as a `cJSON` wrapper
- Added `int64_t` support for `Preferences`
- Added `int64_t` support for `Bundle`
- Added dependencies: `cJSON`, `esp-tls`
- When time is synced via NTP, disable time sync.
- Added docs to 'tt::file::` functions
- Added `tt::string::join()` that works with `std::vector<const char*>`
- Fixed `tt::file::getLastPathSegment()` for the scenario when a path was passed with only a single segment
- Set `CONFIG_ESP_MAIN_TASK_STACK_SIZE=5120` (from about 3k) for all boards
- Set `CONFIG_MBEDTLS_SSL_PROTO_TLS1_3=y` for all boards
This commit is contained in:
Ken Van Hoeylandt 2025-10-25 00:20:48 +02:00 committed by GitHub
parent e9384e0c11
commit f660550f86
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
82 changed files with 1237 additions and 82 deletions

View File

@ -36,14 +36,16 @@ runs:
WLR_LIBINPUT_NO_DEVICES: 1
WAYLAND_DISPLAY: wayland-1
GTK_USE_PORTAL: 0
- name: "Configure Project"
uses: threeal/cmake-action@v1.3.0
- name: Setup cmake
uses: jwlawson/actions-setup-cmake@v2
with:
cmake-version: '3.31.x'
- name: "Prepare Project"
shell: bash
run: cmake -S ./ -B buildsim
- name: "Build Tests"
shell: bash
run: cmake --build buildsim --target AppSim
run: cmake --build buildsim --target FirmwareSim
- name: 'Release'
shell: bash
run: Buildscripts/release-simulator.sh buildsim release/Simulator-${{ inputs.os_name }}-${{ inputs.platform_name }}

3
.gitmodules vendored
View File

@ -13,3 +13,6 @@
[submodule "Libraries/minitar/minitar"]
path = Libraries/minitar/minitar
url = https://github.com/ByteWelder/minitar.git
[submodule "Libraries/cJSON/cJSON"]
path = Libraries/cJSON/cJSON
url = git@github.com:DaveGamble/cJSON.git

View File

@ -12,6 +12,6 @@ target_path=$2
mkdir -p $target_path
cp version.txt $target_path
cp $build_path/App/AppSim $target_path/
cp $build_path/Firmware/FirmwareSim $target_path/
cp -r Data/data $target_path/
cp -r Data/system $target_path/

View File

@ -77,6 +77,8 @@ if (NOT DEFINED ENV{ESP_IDF_VERSION})
# FreeRTOS
set(FREERTOS_CONFIG_FILE_DIRECTORY ${PROJECT_SOURCE_DIR}/Boards/Simulator/Source CACHE STRING "")
set(FREERTOS_PORT GCC_POSIX CACHE STRING "")
add_subdirectory(Libraries/cJSON)
add_subdirectory(Libraries/FreeRTOS-Kernel)
add_subdirectory(Libraries/lv_screenshot)
add_subdirectory(Libraries/QRCode)
@ -92,18 +94,20 @@ if (NOT DEFINED ENV{ESP_IDF_VERSION})
set(ENABLE_PROGRAMS OFF)
add_subdirectory(Libraries/mbedtls)
# SDL
set(SDL_STATIC ON CACHE BOOL "" FORCE)
set(SDL_SHARED OFF CACHE BOOL "" FORCE)
add_subdirectory(Libraries/SDL) # Added as idf component for ESP and as library for other targets
# LVGL
add_compile_definitions($<$<BOOL:${LV_USE_DRAW_SDL}>:LV_USE_DRAW_SDL=1>)
add_subdirectory(Libraries/lvgl) # Added as idf component for ESP and as library for other targets
target_link_libraries(lvgl PRIVATE SDL2-static)
# Sim app
add_subdirectory(Firmware)
# Tests
add_subdirectory(Tests)
# SDL
add_compile_definitions($<$<BOOL:${LV_USE_DRAW_SDL}>:LV_USE_DRAW_SDL=1>)
add_subdirectory(Libraries/SDL) # Added as idf component for ESP and as library for other targets
# LVGL
add_subdirectory(Libraries/lvgl) # Added as idf component for ESP and as library for other targets
target_link_libraries(lvgl PRIVATE SDL2-static)
endif ()

View File

@ -0,0 +1,17 @@
-----BEGIN CERTIFICATE-----
MIICnzCCAiWgAwIBAgIQf/MZd5csIkp2FV0TttaF4zAKBggqhkjOPQQDAzBHMQsw
CQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEU
MBIGA1UEAxMLR1RTIFJvb3QgUjQwHhcNMjMxMjEzMDkwMDAwWhcNMjkwMjIwMTQw
MDAwWjA7MQswCQYDVQQGEwJVUzEeMBwGA1UEChMVR29vZ2xlIFRydXN0IFNlcnZp
Y2VzMQwwCgYDVQQDEwNXRTEwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAARvzTr+
Z1dHTCEDhUDCR127WEcPQMFcF4XGGTfn1XzthkubgdnXGhOlCgP4mMTG6J7/EFmP
LCaY9eYmJbsPAvpWo4H+MIH7MA4GA1UdDwEB/wQEAwIBhjAdBgNVHSUEFjAUBggr
BgEFBQcDAQYIKwYBBQUHAwIwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQU
kHeSNWfE/6jMqeZ72YB5e8yT+TgwHwYDVR0jBBgwFoAUgEzW63T/STaj1dj8tT7F
avCUHYwwNAYIKwYBBQUHAQEEKDAmMCQGCCsGAQUFBzAChhhodHRwOi8vaS5wa2ku
Z29vZy9yNC5jcnQwKwYDVR0fBCQwIjAgoB6gHIYaaHR0cDovL2MucGtpLmdvb2cv
ci9yNC5jcmwwEwYDVR0gBAwwCjAIBgZngQwBAgEwCgYIKoZIzj0EAwMDaAAwZQIx
AOcCq1HW90OVznX+0RGU1cxAQXomvtgM8zItPZCuFQ8jSBJSjz5keROv9aYsAm5V
sQIwJonMaAFi54mrfhfoFNZEfuNMSQ6/bIBiNLiyoX46FohQvKeIoJ99cx7sUkFN
7uJW
-----END CERTIFICATE-----

View File

@ -9,6 +9,7 @@
## Higher Priority
- Calculator bugs (see GitHub issue)
- Store last synced timestamp in NVS (see how HTTPS client example app)
- External app loading: Check the version of Tactility and check ESP target hardware to check for compatibility
Check during installation process, but also when starting (SD card might have old app install from before Tactility OS update)
- Make a URL handler. Use it for handling local files. Match file types with apps.

View File

@ -15,8 +15,8 @@ if (DEFINED ENV{ESP_IDF_VERSION})
)
else ()
add_executable(AppSim ${SOURCE_FILES})
target_link_libraries(AppSim
add_executable(FirmwareSim ${SOURCE_FILES})
target_link_libraries(FirmwareSim
PRIVATE Tactility
PRIVATE TactilityCore
PRIVATE Simulator

View File

@ -0,0 +1,6 @@
file(GLOB SOURCES "cJSON/*.c")
add_library(cJSON STATIC)
target_sources(cJSON PRIVATE ${SOURCES})
include_directories(cJSON "cJSON/")
target_include_directories(cJSON PUBLIC "cJSON/")

View File

@ -0,0 +1,3 @@
# cJSON
This wrapper exists because the original CMake scripts break the builds for Linux and macOS (linker errors)

1
Libraries/cJSON/cJSON Submodule

@ -0,0 +1 @@
Subproject commit acc76239bee01d8e9c858ae2cab296704e52d916

View File

@ -15,8 +15,10 @@ if (DEFINED ENV{ESP_IDF_VERSION})
QRCode
esp_http_server
esp_http_client
esp-tls
esp_lvgl_port
esp_wifi
json
minitar
minmea
nvs_flash
@ -68,6 +70,7 @@ else()
add_definitions(-D_Nonnull=)
target_link_libraries(Tactility
PUBLIC cJSON
PUBLIC TactilityCore
PUBLIC freertos_kernel
PUBLIC lvgl

View File

@ -24,14 +24,17 @@ public:
bool hasBool(const std::string& key) const;
bool hasInt32(const std::string& key) const;
bool hasInt64(const std::string& key) const;
bool hasString(const std::string& key) const;
bool optBool(const std::string& key, bool& out) const;
bool optInt32(const std::string& key, int32_t& out) const;
bool optInt64(const std::string& key, int64_t& out) const;
bool optString(const std::string& key, std::string& out) const;
void putBool(const std::string& key, bool value);
void putInt32(const std::string& key, int32_t value);
void putInt64(const std::string& key, int64_t value);
void putString(const std::string& key, const std::string& value);
};

View File

@ -9,18 +9,18 @@ namespace tt::app {
struct AppManifest;
/** Register an application with its manifest */
void addApp(const AppManifest& manifest);
void addAppManifest(const AppManifest& manifest);
/** Remove an app from the registry */
bool removeApp(const std::string& id);
bool removeAppManifest(const std::string& id);
/** Find an application manifest by its id
* @param[in] id the manifest id
* @return the application manifest if it was found
*/
_Nullable std::shared_ptr<AppManifest> findAppById(const std::string& id);
_Nullable std::shared_ptr<AppManifest> findAppManifestById(const std::string& id);
/** @return a list of all registered apps. This includes user and system apps. */
std::vector<std::shared_ptr<AppManifest>> getApps();
std::vector<std::shared_ptr<AppManifest>> getAppManifests();
} // namespace

View File

@ -22,6 +22,14 @@ namespace tt::app::alertdialog {
* @return the launch id
*/
LaunchId start(const std::string& title, const std::string& message, const std::vector<std::string>& buttonLabels);
/**
* Show a dialog with the provided title, message and 0, 1 or more buttons.
* @param[in] title the title to show in the toolbar
* @param[in] message the message to display
* @param[in] buttonLabels the buttons to show
* @return the launch id
*/
LaunchId start(const std::string& title, const std::string& message, const std::vector<const char*>& buttonLabels);
/**
* Show a dialog with the provided title, message and an OK button

View File

@ -6,7 +6,6 @@
#include <lvgl.h>
namespace tt::lvgl {
#define TOOLBAR_ACTION_LIMIT 4
/** Create a toolbar widget that shows the app name as title */
@ -60,4 +59,10 @@ lv_obj_t* toolbar_add_switch_action(lv_obj_t* obj);
*/
lv_obj_t* toolbar_add_spinner_action(lv_obj_t* obj);
/**
* Remove all actions from the toolbar
* @param[in] obj the toolbar instance
*/
void toolbar_clear_actions(lv_obj_t* obj);
} // namespace

View File

@ -0,0 +1,104 @@
#pragma once
#ifdef ESP_PLATFORM
#include <esp_http_client.h>
namespace tt::network {
class EspHttpClient {
static constexpr auto* TAG = "EspHttpClient";
std::unique_ptr<esp_http_client_config_t> config = nullptr;
esp_http_client_handle_t client;
bool isOpen = false;
public:
~EspHttpClient() {
if (isOpen) {
esp_http_client_close(client);
}
if (client != nullptr) {
esp_http_client_cleanup(client);
}
}
bool init(std::unique_ptr<esp_http_client_config_t> inConfig) {
TT_LOG_I(TAG, "init(%s)", inConfig->url);
assert(this->config == nullptr);
config = std::move(inConfig);
client = esp_http_client_init(config.get());
return client != nullptr;
}
bool open() {
assert(client != nullptr);
TT_LOG_I(TAG, "open()");
auto result = esp_http_client_open(client, 0);
if (result != ESP_OK) {
TT_LOG_E(TAG, "open() failed: %s", esp_err_to_name(result));
return false;
}
isOpen = true;
return true;
}
bool fetchHeaders() const {
assert(client != nullptr);
TT_LOG_I(TAG, "fetchHeaders()");
return esp_http_client_fetch_headers(client) >= 0;
}
bool isStatusCodeOk() const {
assert(client != nullptr);
const auto status_code = getStatusCode();
return status_code >= 200 && status_code < 300;
}
int getStatusCode() const {
assert(client != nullptr);
const auto status_code = esp_http_client_get_status_code(client);
TT_LOG_I(TAG, "Status code %d", status_code);
return status_code;
}
int getContentLength() const {
assert(client != nullptr);
return esp_http_client_get_content_length(client);
}
int read(char* bytes, int size) const {
assert(client != nullptr);
TT_LOG_I(TAG, "read(%d)", size);
return esp_http_client_read(client, bytes, size);
}
int readResponse(char* bytes, int size) const {
assert(client != nullptr);
TT_LOG_I(TAG, "readResponse(%d)", size);
return esp_http_client_read_response(client, bytes, size);
}
bool close() {
assert(client != nullptr);
TT_LOG_I(TAG, "close()");
return esp_http_client_close(client) == ESP_OK;
}
bool cleanup() {
assert(client != nullptr);
TT_LOG_I(TAG, "cleanup()");
const auto result = esp_http_client_cleanup(client);
client = nullptr;
return result == ESP_OK;
}
};
}
#endif

View File

@ -0,0 +1,15 @@
#pragma once
#include <string>
namespace tt::network::http {
void download(
const std::string& url,
const std::string& certFilePath,
const std::string &downloadFilePath,
std::function<void()> onSuccess,
std::function<void(const char* errorMessage)> onError
);
}

View File

@ -0,0 +1,7 @@
#pragma once
namespace tt::network::ntp {
bool isSynced();
}

View File

@ -0,0 +1,13 @@
#pragma once
#include <string>
namespace tt::app::apphub {
constexpr auto* CERTIFICATE_PATH = "/system/certificates/WE1.pem";
std::string getAppsJsonUrl();
std::string getDownloadUrl(const std::string& relativePath);
}

View File

@ -0,0 +1,21 @@
#pragma once
#include <string>
#include <vector>
namespace tt::app::apphub {
struct AppHubEntry {
std::string appId;
std::string appVersionName;
int32_t appVersionCode;
std::string appName;
std::string appDescription;
std::string targetSdk;
std::vector<std::string> targetPlatforms;
std::string file;
};
bool parseJson(const std::string& filePath, std::vector<AppHubEntry>& entries);
}

View File

@ -0,0 +1,9 @@
#pragma once
#include <Tactility/app/apphub/AppHubEntry.h>
namespace tt::app::apphubdetails {
void start(const apphub::AppHubEntry& entry);
} // namespace

View File

@ -0,0 +1,77 @@
#pragma once
#include <cJSON.h>
#include <string>
#include <vector>
namespace tt::json {
class Reader {
const cJSON* root;
static constexpr const char* TAG = "json::Reader";
public:
explicit Reader(const cJSON* root) : root(root) {}
bool readString(const char* key, std::string& output) const {
const auto* child = cJSON_GetObjectItemCaseSensitive(root, key);
if (!cJSON_IsString(child)) {
TT_LOG_E(TAG, "%s is not a string", key);
return false;
}
output = cJSON_GetStringValue(child);
return true;
}
bool readInt32(const char* key, int32_t& output) const {
double buffer;
if (!readNumber(key, buffer)) {
return false;
}
output = buffer;
return true;
}
bool readInt(const char* key, int& output) const {
double buffer;
if (!readNumber(key, buffer)) {
return false;
}
output = buffer;
return true;
}
bool readNumber(const char* key, double& output) const {
const auto* child = cJSON_GetObjectItemCaseSensitive(root, key);
if (!cJSON_IsNumber(child)) {
TT_LOG_E(TAG, "%s is not a string", key);
return false;
}
output = cJSON_GetNumberValue(child);
return true;
}
bool readStringArray(const char* key, std::vector<std::string>& output) const {
const auto* child = cJSON_GetObjectItemCaseSensitive(root, key);
if (!cJSON_IsArray(child)) {
TT_LOG_E(TAG, "%s is not an array", key);
return false;
}
const auto size = cJSON_GetArraySize(child);
TT_LOG_I(TAG, "Processing %d array children", size);
output.resize(size);
for (int i = 0; i < size; ++i) {
const auto string_json = cJSON_GetArrayItem(child, i);
if (!cJSON_IsString(string_json)) {
TT_LOG_E(TAG, "array child of %s is not a string", key);
return false;
}
output[i] = cJSON_GetStringValue(string_json);
}
return true;
}
};
}

View File

@ -1,14 +1,14 @@
#ifdef ESP_PLATFORM
#include "nvs_flash.h"
#include "Tactility/Preferences.h"
#include <Tactility/Preferences.h>
#include <Tactility/TactilityCore.h>
#define TAG "preferences"
#include <nvs_flash.h>
namespace tt {
constexpr auto* TAG = "Preferences";
bool Preferences::optBool(const std::string& key, bool& out) const {
nvs_handle_t handle;
if (nvs_open(namespace_, NVS_READWRITE, &handle) != ESP_OK) {
@ -37,6 +37,18 @@ bool Preferences::optInt32(const std::string& key, int32_t& out) const {
}
}
bool Preferences::optInt64(const std::string& key, int64_t& out) const {
nvs_handle_t handle;
if (nvs_open(namespace_, NVS_READWRITE, &handle) != ESP_OK) {
TT_LOG_E(TAG, "Failed to open namespace %s", namespace_);
return false;
} else {
bool success = nvs_get_i64(handle, key.c_str(), &out) == ESP_OK;
nvs_close(handle);
return success;
}
}
bool Preferences::optString(const std::string& key, std::string& out) const {
nvs_handle_t handle;
if (nvs_open(namespace_, NVS_READWRITE, &handle) != ESP_OK) {
@ -63,6 +75,11 @@ bool Preferences::hasInt32(const std::string& key) const {
return optInt32(key, temp);
}
bool Preferences::hasInt64(const std::string& key) const {
int64_t temp;
return optInt64(key, temp);
}
bool Preferences::hasString(const std::string& key) const {
std::string temp;
return optString(key, temp);
@ -92,6 +109,18 @@ void Preferences::putInt32(const std::string& key, int32_t value) {
}
}
void Preferences::putInt64(const std::string& key, int64_t value) {
nvs_handle_t handle;
if (nvs_open(namespace_, NVS_READWRITE, &handle) == ESP_OK) {
if (nvs_set_i64(handle, key.c_str(), value) != ESP_OK) {
TT_LOG_E(TAG, "Failed to write %s:%s", namespace_, key.c_str());
}
nvs_close(handle);
} else {
TT_LOG_E(TAG, "Failed to open namespace %s", namespace_);
}
}
void Preferences::putString(const std::string& key, const std::string& text) {
nvs_handle_t handle;
if (nvs_open(namespace_, NVS_READWRITE, &handle) == ESP_OK) {

View File

@ -30,6 +30,11 @@ bool Preferences::hasInt32(const std::string& key) const {
return preferences.hasInt32(bundle_key);
}
bool Preferences::hasInt64(const std::string& key) const {
std::string bundle_key = get_bundle_key(namespace_, key);
return preferences.hasInt64(bundle_key);
}
bool Preferences::hasString(const std::string& key) const {
std::string bundle_key = get_bundle_key(namespace_, key);
return preferences.hasString(bundle_key);
@ -45,6 +50,11 @@ bool Preferences::optInt32(const std::string& key, int32_t& out) const {
return preferences.optInt32(bundle_key, out);
}
bool Preferences::optInt64(const std::string& key, int64_t& out) const {
std::string bundle_key = get_bundle_key(namespace_, key);
return preferences.optInt64(bundle_key, out);
}
bool Preferences::optString(const std::string& key, std::string& out) const {
std::string bundle_key = get_bundle_key(namespace_, key);
return preferences.optString(bundle_key, out);
@ -60,6 +70,11 @@ void Preferences::putInt32(const std::string& key, int32_t value) {
return preferences.putInt32(bundle_key, value);
}
void Preferences::putInt64(const std::string& key, int64_t value) {
std::string bundle_key = get_bundle_key(namespace_, key);
return preferences.putInt64(bundle_key, value);
}
void Preferences::putString(const std::string& key, const std::string& value) {
std::string bundle_key = get_bundle_key(namespace_, key);
return preferences.putString(bundle_key, value);

View File

@ -56,6 +56,8 @@ namespace service {
namespace app {
namespace addgps { extern const AppManifest manifest; }
namespace apphub { extern const AppManifest manifest; }
namespace apphubdetails { extern const AppManifest manifest; }
namespace alertdialog { extern const AppManifest manifest; }
namespace appdetails { extern const AppManifest manifest; }
namespace applist { extern const AppManifest manifest; }
@ -99,53 +101,55 @@ namespace app {
// List of all apps excluding Boot app (as Boot app calls this function indirectly)
static void registerInternalApps() {
addApp(app::alertdialog::manifest);
addApp(app::appdetails::manifest);
addApp(app::applist::manifest);
addApp(app::appsettings::manifest);
addApp(app::display::manifest);
addApp(app::files::manifest);
addApp(app::fileselection::manifest);
addApp(app::imageviewer::manifest);
addApp(app::inputdialog::manifest);
addApp(app::launcher::manifest);
addApp(app::localesettings::manifest);
addApp(app::notes::manifest);
addApp(app::settings::manifest);
addApp(app::selectiondialog::manifest);
addApp(app::systeminfo::manifest);
addApp(app::timedatesettings::manifest);
addApp(app::timezone::manifest);
addApp(app::wifiapsettings::manifest);
addApp(app::wificonnect::manifest);
addApp(app::wifimanage::manifest);
addAppManifest(app::alertdialog::manifest);
addAppManifest(app::appdetails::manifest);
addAppManifest(app::apphub::manifest);
addAppManifest(app::apphubdetails::manifest);
addAppManifest(app::applist::manifest);
addAppManifest(app::appsettings::manifest);
addAppManifest(app::display::manifest);
addAppManifest(app::files::manifest);
addAppManifest(app::fileselection::manifest);
addAppManifest(app::imageviewer::manifest);
addAppManifest(app::inputdialog::manifest);
addAppManifest(app::launcher::manifest);
addAppManifest(app::localesettings::manifest);
addAppManifest(app::notes::manifest);
addAppManifest(app::settings::manifest);
addAppManifest(app::selectiondialog::manifest);
addAppManifest(app::systeminfo::manifest);
addAppManifest(app::timedatesettings::manifest);
addAppManifest(app::timezone::manifest);
addAppManifest(app::wifiapsettings::manifest);
addAppManifest(app::wificonnect::manifest);
addAppManifest(app::wifimanage::manifest);
#if defined(CONFIG_TINYUSB_MSC_ENABLED) && CONFIG_TINYUSB_MSC_ENABLED
addApp(app::usbsettings::manifest);
addAppManifest(app::usbsettings::manifest);
#endif
#if TT_FEATURE_SCREENSHOT_ENABLED
addApp(app::screenshot::manifest);
addAppManifest(app::screenshot::manifest);
#endif
#ifdef ESP_PLATFORM
addApp(app::chat::manifest);
addApp(app::crashdiagnostics::manifest);
addApp(app::development::manifest);
addAppManifest(app::chat::manifest);
addAppManifest(app::crashdiagnostics::manifest);
addAppManifest(app::development::manifest);
#endif
if (!hal::getConfiguration()->i2c.empty()) {
addApp(app::i2cscanner::manifest);
addApp(app::i2csettings::manifest);
addAppManifest(app::i2cscanner::manifest);
addAppManifest(app::i2csettings::manifest);
}
if (!hal::getConfiguration()->uart.empty()) {
addApp(app::addgps::manifest);
addApp(app::gpssettings::manifest);
addAppManifest(app::addgps::manifest);
addAppManifest(app::gpssettings::manifest);
}
if (hal::hasDevice(hal::Device::Type::Power)) {
addApp(app::power::manifest);
addAppManifest(app::power::manifest);
}
}
@ -172,7 +176,7 @@ static void registerInstalledApp(std::string path) {
manifest.appCategory = app::Category::User;
manifest.appLocation = app::Location::external(path);
app::addApp(manifest);
app::addAppManifest(manifest);
}
static void registerInstalledApps(const std::string& path) {
@ -266,7 +270,7 @@ void run(const Configuration& config) {
TT_LOG_I(TAG, "Starting boot app");
// The boot app takes care of registering system apps, user services and user apps
addApp(app::boot::manifest);
addAppManifest(app::boot::manifest);
app::start(app::boot::manifest.appId);
TT_LOG_I(TAG, "Main dispatcher ready");

View File

@ -188,7 +188,7 @@ bool install(const std::string& path) {
manifest.appLocation = Location::external(renamed_target_path);
addApp(manifest);
addAppManifest(manifest);
return true;
}
@ -211,7 +211,7 @@ bool uninstall(const std::string& appId) {
return false;
}
if (!removeApp(appId)) {
if (!removeAppManifest(appId)) {
TT_LOG_W(TAG, "Failed to remove app %s from registry", appId.c_str());
}

View File

@ -15,7 +15,7 @@ typedef std::unordered_map<std::string, std::shared_ptr<AppManifest>> AppManifes
static AppManifestMap app_manifest_map;
static Mutex hash_mutex(Mutex::Type::Normal);
void addApp(const AppManifest& manifest) {
void addAppManifest(const AppManifest& manifest) {
TT_LOG_I(TAG, "Registering manifest %s", manifest.appId.c_str());
hash_mutex.lock();
@ -29,7 +29,7 @@ void addApp(const AppManifest& manifest) {
hash_mutex.unlock();
}
bool removeApp(const std::string& id) {
bool removeAppManifest(const std::string& id) {
TT_LOG_I(TAG, "Removing manifest for %s", id.c_str());
auto lock = hash_mutex.asScopedLock();
@ -38,7 +38,7 @@ bool removeApp(const std::string& id) {
return app_manifest_map.erase(id) == 1;
}
_Nullable std::shared_ptr<AppManifest> findAppById(const std::string& id) {
_Nullable std::shared_ptr<AppManifest> findAppManifestById(const std::string& id) {
hash_mutex.lock();
auto result = app_manifest_map.find(id);
hash_mutex.unlock();
@ -49,7 +49,7 @@ _Nullable std::shared_ptr<AppManifest> findAppById(const std::string& id) {
}
}
std::vector<std::shared_ptr<AppManifest>> getApps() {
std::vector<std::shared_ptr<AppManifest>> getAppManifests() {
std::vector<std::shared_ptr<AppManifest>> manifests;
hash_mutex.lock();
for (const auto& item: app_manifest_map) {

View File

@ -31,6 +31,15 @@ LaunchId start(const std::string& title, const std::string& message, const std::
return app::start(manifest.appId, bundle);
}
LaunchId start(const std::string& title, const std::string& message, const std::vector<const char*>& buttonLabels) {
std::string items_joined = string::join(buttonLabels, PARAMETER_ITEM_CONCATENATION_TOKEN);
auto bundle = std::make_shared<Bundle>();
bundle->putString(PARAMETER_BUNDLE_KEY_TITLE, title);
bundle->putString(PARAMETER_BUNDLE_KEY_MESSAGE, message);
bundle->putString(PARAMETER_BUNDLE_KEY_BUTTON_LABELS, items_joined);
return app::start(manifest.appId, bundle);
}
LaunchId start(const std::string& title, const std::string& message) {
auto bundle = std::make_shared<Bundle>();
bundle->putString(PARAMETER_BUNDLE_KEY_TITLE, title);
@ -57,8 +66,6 @@ static std::string getTitleParameter(std::shared_ptr<const Bundle> bundle) {
class AlertDialogApp : public App {
private:
static void onButtonClickedCallback(lv_event_t* e) {
auto app = std::static_pointer_cast<AlertDialogApp>(getCurrentApp());
assert(app != nullptr);
@ -66,7 +73,6 @@ private:
}
void onButtonClicked(lv_event_t* e) {
lv_event_code_t code = lv_event_get_code(e);
auto index = reinterpret_cast<std::size_t>(lv_event_get_user_data(e));
TT_LOG_I(TAG, "Selected item at index %d", index);

View File

@ -42,7 +42,7 @@ public:
const auto parameters = app.getParameters();
tt_check(parameters != nullptr, "Parameters missing");
auto app_id = parameters->getString("appId");
manifest = findAppById(app_id);
manifest = findAppManifestById(app_id);
assert(manifest != nullptr);
}

View File

@ -0,0 +1,27 @@
#include <Tactility/app/apphub/AppHub.h>
#include <format>
namespace tt::app::apphub {
constexpr auto* BASE_URL = "https://cdn.tactility.one/apps";
static std::string getVersionWithoutPostfix() {
std::string version(TT_VERSION);
auto index = version.find_first_of('-');
if (index == std::string::npos) {
return version;
} else {
return version.substr(0, index);
}
}
std::string getAppsJsonUrl() {
return std::format("{}/{}/apps.json", BASE_URL, getVersionWithoutPostfix());
}
std::string getDownloadUrl(const std::string& relativePath) {
return std::format("{}/{}/{}", BASE_URL, getVersionWithoutPostfix(), relativePath);
}
}

View File

@ -0,0 +1,186 @@
#include <Tactility/app/apphub/AppHub.h>
#include <Tactility/app/apphub/AppHubEntry.h>
#include <Tactility/app/apphubdetails/AppHubDetailsApp.h>
#include <Tactility/file/File.h>
#include <Tactility/lvgl/LvglSync.h>
#include <Tactility/lvgl/Spinner.h>
#include <Tactility/lvgl/Toolbar.h>
#include <Tactility/network/Http.h>
#include <Tactility/Paths.h>
#include <Tactility/service/loader/Loader.h>
#include <Tactility/service/wifi/Wifi.h>
#include <lvgl.h>
#include <format>
namespace tt::app::apphub {
constexpr auto* TAG = "AppHub";
extern const AppManifest manifest;
class AppHubApp final : public App {
lv_obj_t* contentWrapper = nullptr;
lv_obj_t* refreshButton = nullptr;
std::string cachedAppsJsonFile = std::format("{}/app_hub.json", getTempPath());
std::unique_ptr<Thread> thread;
std::vector<AppHubEntry> entries;
Mutex mutex;
static std::shared_ptr<AppHubApp> _Nullable findAppInstance() {
auto app_context = getCurrentAppContext();
if (app_context->getManifest().appId != manifest.appId) {
return nullptr;
}
return std::static_pointer_cast<AppHubApp>(app_context->getApp());
}
static void onAppPressed(lv_event_t* e) {
const auto* self = static_cast<AppHubApp*>(lv_event_get_user_data(e));
auto* widget = lv_event_get_target_obj(e);
const auto* user_data = lv_obj_get_user_data(widget);
#ifdef ESP_PLATFORM
const int index = reinterpret_cast<int>(user_data);
#else
const long long index = reinterpret_cast<long long>(user_data);
#endif
self->mutex.lock();
if (index < self->entries.size()) {
apphubdetails::start(self->entries[index]);
}
self->mutex.unlock();
}
static void onRefreshPressed(lv_event_t* e) {
auto* self = static_cast<AppHubApp*>(lv_event_get_user_data(e));
self->refresh();
}
void onRefreshSuccess() {
TT_LOG_I(TAG, "Request success");
auto lock = lvgl::getSyncLock()->asScopedLock();
lock.lock();
showApps();
}
void onRefreshError(const char* error) {
TT_LOG_E(TAG, "Request failed: %s", error);
auto lock = lvgl::getSyncLock()->asScopedLock();
lock.lock();
showRefreshFailedError("Cannot reach server");
}
static void createAppWidget(const std::shared_ptr<AppManifest>& manifest, lv_obj_t* list) {
lv_obj_t* btn = lv_list_add_button(list, nullptr, manifest->appName.c_str());
lv_obj_add_event_cb(btn, &onAppPressed, LV_EVENT_SHORT_CLICKED, manifest.get());
}
void showRefreshFailedError(const char* message) {
lv_obj_clean(contentWrapper);
auto* label = lv_label_create(contentWrapper);
lv_label_set_text(label, message);
lv_obj_align(label, LV_ALIGN_CENTER, 0, 0);
lv_obj_remove_flag(refreshButton, LV_OBJ_FLAG_HIDDEN);
}
void showNoInternet() {
showRefreshFailedError("No Internet Connection");
}
void showTimeNotSynced() {
showRefreshFailedError("Time is not synced yet.\nIt's required to establish a secure connection.");
}
void showApps() {
lv_obj_clean(contentWrapper);
mutex.lock();
if (parseJson(cachedAppsJsonFile, entries)) {
std::ranges::sort(entries, [](auto left, auto right) {
return left.appName < right.appName;
});
auto* list = lv_list_create(contentWrapper);
lv_obj_set_style_pad_all(list, 0, LV_STATE_DEFAULT);
lv_obj_set_size(list, LV_PCT(100), LV_SIZE_CONTENT);
for (int i = 0; i < entries.size(); i++) {
auto& entry = entries[i];
TT_LOG_I(TAG, "Adding %s", entry.appName.c_str());
const char* icon = findAppManifestById(entry.appId) != nullptr ? LV_SYMBOL_OK : nullptr;
auto* entry_button = lv_list_add_button(list, icon, entry.appName.c_str());
lv_obj_set_user_data(entry_button, reinterpret_cast<void*>(i));
lv_obj_add_event_cb(entry_button, onAppPressed, LV_EVENT_SHORT_CLICKED, this);
}
} else {
showRefreshFailedError("Failed to load content");
}
mutex.unlock();
}
void refresh() {
lv_obj_clean(contentWrapper);
auto* spinner = lvgl::spinner_create(contentWrapper);
lv_obj_align(spinner, LV_ALIGN_CENTER, 0, 0);
lv_obj_add_flag(refreshButton, LV_OBJ_FLAG_HIDDEN);
if (service::wifi::getRadioState() != service::wifi::RadioState::ConnectionActive) {
showNoInternet();
return;
}
if (file::isFile(cachedAppsJsonFile)) {
showApps();
}
network::http::download(
getAppsJsonUrl(),
CERTIFICATE_PATH,
cachedAppsJsonFile,
[] {
auto app = findAppInstance();
if (app != nullptr) {
app->onRefreshSuccess();
}
},
[](const char* error) {
auto app = findAppInstance();
if (app != nullptr) {
app->onRefreshError(error);
}
}
);
}
public:
void onShow(TT_UNUSED AppContext& app, lv_obj_t* parent) override {
lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN);
lv_obj_set_style_pad_row(parent, 0, LV_STATE_DEFAULT);
auto* toolbar = lvgl::toolbar_create(parent, app);
refreshButton = lvgl::toolbar_add_image_button_action(toolbar, LV_SYMBOL_REFRESH, onRefreshPressed, this);
lv_obj_add_flag(refreshButton, LV_OBJ_FLAG_HIDDEN);
contentWrapper = lv_obj_create(parent);
lv_obj_set_width(contentWrapper, LV_PCT(100));
lv_obj_set_flex_grow(contentWrapper, 1);
lv_obj_set_style_pad_all(contentWrapper, 0, LV_STATE_DEFAULT);
lv_obj_set_style_pad_ver(contentWrapper, 0, LV_STATE_DEFAULT);
refresh();
}
};
extern const AppManifest manifest = {
.appId = "AppHub",
.appName = "App Hub",
.appCategory = Category::System,
.createApp = create<AppHubApp>,
};
} // namespace

View File

@ -0,0 +1,56 @@
#include <Tactility/app/apphub/AppHubEntry.h>
#include <Tactility/file/File.h>
#include <Tactility/json/Reader.h>
namespace tt::app::apphub {
constexpr auto* TAG = "AppHubJson";
static bool parseEntry(const cJSON* object, AppHubEntry& entry) {
const json::Reader reader(object);
return reader.readString("appId", entry.appId) &&
reader.readString("appVersionName", entry.appVersionName) &&
reader.readInt32("appVersionCode", entry.appVersionCode) &&
reader.readString("appName", entry.appName) &&
reader.readString("appDescription", entry.appDescription) &&
reader.readString("targetSdk", entry.targetSdk) &&
reader.readString("file", entry.file) &&
reader.readStringArray("targetPlatforms", entry.targetPlatforms);
}
bool parseJson(const std::string& filePath, std::vector<AppHubEntry>& entries) {
auto lock = file::getLock(filePath)->asScopedLock();
lock.lock();
auto data = file::readString(filePath);
auto data_ptr = reinterpret_cast<const char*>(data.get());
auto* json = cJSON_Parse(data_ptr);
if (json == nullptr) {
TT_LOG_E(TAG, "Failed to parse %s", filePath.c_str());
return false;
}
const cJSON* apps_json = cJSON_GetObjectItemCaseSensitive(json, "apps");
if (!cJSON_IsArray(apps_json)) {
cJSON_Delete(json);
TT_LOG_E(TAG, "apps is not an array");
return false;
}
auto apps_size = cJSON_GetArraySize(apps_json);
entries.resize(apps_size);
for (int i = 0; i < apps_size; ++i) {
auto& entry = entries.at(i);
auto* entry_json = cJSON_GetArrayItem(apps_json, i);
if (!parseEntry(entry_json, entry)) {
TT_LOG_E(TAG, "Failed to read entry");
cJSON_Delete(json);
return false;
}
}
cJSON_Delete(json);
return true;
}
}

View File

@ -0,0 +1,248 @@
#include <Tactility/app/alertdialog/AlertDialog.h>
#include <Tactility/app/apphub/AppHub.h>
#include <Tactility/app/apphub/AppHubEntry.h>
#include <Tactility/app/AppRegistration.h>
#include <Tactility/file/File.h>
#include <Tactility/lvgl/LvglSync.h>
#include <Tactility/lvgl/Toolbar.h>
#include <Tactility/network/Http.h>
#include <Tactility/Paths.h>
#include <Tactility/service/loader/Loader.h>
#include <Tactility/StringUtils.h>
#include <lvgl.h>
#include <format>
namespace tt::app::apphubdetails {
extern const AppManifest manifest;
constexpr auto* TAG = "AppHubDetails";
static std::shared_ptr<Bundle> toBundle(const apphub::AppHubEntry& entry) {
auto bundle = std::make_shared<Bundle>();
bundle->putString("appId", entry.appId);
bundle->putString("appVersionName", entry.appVersionName);
bundle->putInt32("appVersionCode", entry.appVersionCode);
bundle->putString("appName", entry.appName);
bundle->putString("appDescription", entry.appDescription);
bundle->putString("targetSdk", entry.targetSdk);
bundle->putString("file", entry.file);
bundle->putString("targetPlatforms", string::join(entry.targetPlatforms, ","));
return bundle;
}
static bool fromBundle(const Bundle& bundle, apphub::AppHubEntry& entry) {
std::string target_platforms_string;
auto result = bundle.optString("appId", entry.appId) &&
bundle.optString("appVersionName", entry.appVersionName) &&
bundle.optInt32("appVersionCode", entry.appVersionCode) &&
bundle.optString("appName", entry.appName) &&
bundle.optString("appDescription", entry.appDescription) &&
bundle.optString("targetSdk", entry.targetSdk) &&
bundle.optString("file", entry.file) &&
bundle.optString("targetPlatforms", target_platforms_string);
entry.targetPlatforms = string::split(target_platforms_string, ",");
return result;
}
class AppHubDetailsApp final : public App {
static constexpr auto* CONFIRM_TEXT = "Confirm";
static constexpr auto* CANCEL_TEXT = "Cancel";
static constexpr auto CONFIRMATION_BUTTON_INDEX = 0;
const std::vector<const char*> CONFIRM_CANCEL_LABELS = { CONFIRM_TEXT, CANCEL_TEXT };
apphub::AppHubEntry entry;
std::shared_ptr<AppManifest> entryManifest;
lv_obj_t* toolbar = nullptr;
lv_obj_t* spinner = nullptr;
lv_obj_t* updateButton = nullptr;
lv_obj_t* updateLabel = nullptr;
LaunchId installLaunchId = -1;
LaunchId uninstallLaunchId = -1;
LaunchId updateLaunchId = -1;
LaunchId showConfirmDialog(const char* action) {
const auto message = std::format("{} {}?", action, entry.appName);
return alertdialog::start(CONFIRM_TEXT, message, CONFIRM_CANCEL_LABELS);
}
static void onInstallPressed(lv_event_t* e) {
auto* self = static_cast<AppHubDetailsApp*>(lv_event_get_user_data(e));
self->installLaunchId = self->showConfirmDialog("Install");
}
static void onUninstallPressed(lv_event_t* e) {
auto* self = static_cast<AppHubDetailsApp*>(lv_event_get_user_data(e));
self->uninstallLaunchId = self->showConfirmDialog("Uninstall");
}
static void onUpdatePressed(lv_event_t* e) {
auto* self = static_cast<AppHubDetailsApp*>(lv_event_get_user_data(e));
self->updateLaunchId = self->showConfirmDialog("Update");
}
void uninstallApp() {
TT_LOG_I(TAG, "Uninstall");
lvgl::getSyncLock()->lock();
lv_obj_remove_flag(spinner, LV_OBJ_FLAG_HIDDEN);
lvgl::getSyncLock()->unlock();
uninstall(entry.appId);
lvgl::getSyncLock()->lock();
updateViews();
lvgl::getSyncLock()->unlock();
}
void doInstall() {
auto url = apphub::getDownloadUrl(entry.file);
auto file_name = file::getLastPathSegment(entry.file);
auto temp_file_path = std::format("{}/{}", getTempPath(), file_name);
network::http::download(
url,
apphub::CERTIFICATE_PATH,
temp_file_path,
[this, temp_file_path] {
install(temp_file_path);
if (!file::deleteFile(temp_file_path.c_str())) {
TT_LOG_W(TAG, "Failed to remove %s", temp_file_path.c_str());
} else {
TT_LOG_I(TAG, "Deleted temporary file %s", temp_file_path.c_str());
}
lvgl::getSyncLock()->lock();
updateViews();
lvgl::getSyncLock()->unlock();
},
[temp_file_path](const char* errorMessage) {
TT_LOG_E(TAG, "Download failed: %s", errorMessage);
alertdialog::start("Error", "Failed to install app");
if (file::isFile(temp_file_path) && !file::deleteFile(temp_file_path.c_str())) {
TT_LOG_W(TAG, "Failed to remove %s", temp_file_path.c_str());
}
}
);
}
void installApp() {
TT_LOG_I(TAG, "Install");
lvgl::getSyncLock()->lock();
lv_obj_remove_flag(spinner, LV_OBJ_FLAG_HIDDEN);
lvgl::getSyncLock()->unlock();
doInstall();
}
void updateApp() {
TT_LOG_I(TAG, "Update");
lvgl::getSyncLock()->lock();
lv_obj_remove_flag(spinner, LV_OBJ_FLAG_HIDDEN);
lvgl::getSyncLock()->unlock();
TT_LOG_I(TAG, "Removing previous version");
uninstall(entry.appId);
TT_LOG_I(TAG, "Installing new version");
doInstall();
}
void updateViews() {
lvgl::toolbar_clear_actions(toolbar);
const auto manifest = findAppManifestById(entry.appId);
spinner = lvgl::toolbar_add_spinner_action(toolbar);
lv_obj_add_flag(spinner, LV_OBJ_FLAG_HIDDEN);
lv_obj_add_flag(updateLabel, LV_OBJ_FLAG_HIDDEN);
if (manifest != nullptr) {
if (manifest->appVersionCode < entry.appVersionCode) {
updateButton = lvgl::toolbar_add_image_button_action(toolbar, LV_SYMBOL_DOWNLOAD, onUpdatePressed, this);
lv_obj_remove_flag(updateLabel, LV_OBJ_FLAG_HIDDEN);
}
lvgl::toolbar_add_image_button_action(toolbar, LV_SYMBOL_TRASH, onUninstallPressed, this);
} else {
lvgl::toolbar_add_image_button_action(toolbar, LV_SYMBOL_DOWNLOAD, onInstallPressed, this);
}
}
public:
void onCreate(AppContext& appContext) override {
auto parameters = appContext.getParameters();
if (parameters == nullptr) {
TT_LOG_E(TAG, "No parameters");
stop();
return;
}
if (!fromBundle(*parameters.get(), entry)) {
TT_LOG_E(TAG, "Invalid parameters");
stop();
}
}
void onShow(TT_UNUSED AppContext& app, lv_obj_t* parent) override {
lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN);
lv_obj_set_style_pad_row(parent, 0, LV_STATE_DEFAULT);
toolbar = lvgl::toolbar_create(parent, entry.appName.c_str());
auto* wrapper = lv_obj_create(parent);
lv_obj_set_width(wrapper, LV_PCT(100));
lv_obj_set_flex_grow(wrapper, 1);
lv_obj_set_flex_flow(wrapper, LV_FLEX_FLOW_COLUMN);
updateLabel = lv_label_create(wrapper);
lv_label_set_text(updateLabel, "Update available!");
lv_obj_set_style_text_color(updateLabel, lv_color_make(0xff, 0xff, 00), LV_STATE_DEFAULT);
auto* description_label = lv_label_create(wrapper);
lv_obj_set_width(description_label, LV_PCT(100));
lv_label_set_long_mode(description_label, LV_LABEL_LONG_MODE_WRAP);
if (!entry.appDescription.empty()) {
lv_label_set_text(description_label, entry.appDescription.c_str());
} else {
lv_label_set_text(description_label, "This app has no description yet.");
}
auto* version_label = lv_label_create(wrapper);
lv_label_set_text_fmt(version_label, "Version %s", entry.appVersionName.c_str());
updateViews();
}
void onResult(AppContext& appContext, LaunchId launchId, Result result, std::unique_ptr<Bundle> resultData) override {
if (result != Result::Ok) {
return;
}
if (alertdialog::getResultIndex(*resultData.get()) != CONFIRMATION_BUTTON_INDEX) {
return;
}
if (launchId == installLaunchId) {
installApp();
} else if (launchId == uninstallLaunchId) {
uninstallApp();
} else if (launchId == updateLaunchId) {
updateApp();
}
}
};
void start(const apphub::AppHubEntry& entry) {
const auto bundle = toBundle(entry);
app::start(manifest.appId, bundle);
}
extern const AppManifest manifest = {
.appId = "AppHubDetails",
.appName = "App Details",
.appCategory = Category::System,
.appFlags = AppManifest::Flags::Hidden,
.createApp = create<AppHubDetailsApp>,
};
} // namespace

View File

@ -36,7 +36,7 @@ public:
auto parent_content_height = lv_obj_get_content_height(parent);
lv_obj_set_height(list, parent_content_height - toolbar_height);
auto manifests = getApps();
auto manifests = getAppManifests();
std::ranges::sort(manifests, SortAppManifestByName);
for (const auto& manifest: manifests) {

View File

@ -37,7 +37,7 @@ public:
auto parent_content_height = lv_obj_get_content_height(parent);
lv_obj_set_height(list, parent_content_height - toolbar_height);
auto manifests = getApps();
auto manifests = getAppManifests();
std::ranges::sort(manifests, SortAppManifestByName);
size_t app_count = 0;

View File

@ -92,7 +92,8 @@ void View::viewFile(const std::string& path, const std::string& filename) {
// install(filename);
auto message = std::format("Do you want to install {}?", filename);
installAppPath = processed_filepath;
installAppLaunchId = alertdialog::start("Install?", message, { "Yes", "No" });
auto choices = std::vector { "Yes", "No" };
installAppLaunchId = alertdialog::start("Install?", message, choices);
#endif
} else if (isSupportedImageFile(filename)) {
imageviewer::start(processed_filepath);

View File

@ -35,7 +35,7 @@ class SettingsApp final : public App {
lv_obj_set_width(list, LV_PCT(100));
lv_obj_set_flex_grow(list, 1);
auto manifests = getApps();
auto manifests = getAppManifests();
std::ranges::sort(manifests, SortAppManifestByName);
for (const auto& manifest: manifests) {
if (manifest->appCategory == Category::Settings) {

View File

@ -220,4 +220,9 @@ lv_obj_t* toolbar_add_spinner_action(lv_obj_t* obj) {
return spinner;
}
void toolbar_clear_actions(lv_obj_t* obj) {
auto* toolbar = reinterpret_cast<Toolbar*>(obj);
lv_obj_clean(toolbar->action_container);
}
} // namespace

View File

@ -0,0 +1,102 @@
#include <Tactility/Tactility.h>
#include <Tactility/file/File.h>
#include <Tactility/network/Http.h>
#ifdef ESP_PLATFORM
#include <Tactility/network/EspHttpClient.h>
#include <esp_sntp.h>
#include <esp_http_client.h>
#endif
namespace tt::network::http {
constexpr auto* TAG = "HTTP";
void download(
const std::string& url,
const std::string& certFilePath,
const std::string &downloadFilePath,
std::function<void()> onSuccess,
std::function<void(const char* errorMessage)> onError
) {
TT_LOG_I(TAG, "Downloading %s to %s", url.c_str(), downloadFilePath.c_str());
#ifdef ESP_PLATFORM
getMainDispatcher().dispatch([url, certFilePath, downloadFilePath, onSuccess, onError] {
TT_LOG_I(TAG, "Loading certificate");
auto certificate = file::readString(certFilePath);
auto certificate_length = strlen(reinterpret_cast<const char*>(certificate.get())) + 1;
auto config = std::make_unique<esp_http_client_config_t>(esp_http_client_config_t {
.url = url.c_str(),
.auth_type = HTTP_AUTH_TYPE_NONE,
.cert_pem = reinterpret_cast<const char*>(certificate.get()),
.cert_len = certificate_length,
.tls_version = ESP_HTTP_CLIENT_TLS_VER_TLS_1_3,
.method = HTTP_METHOD_GET,
.timeout_ms = 5000,
.transport_type = HTTP_TRANSPORT_OVER_SSL
});
auto client = std::make_unique<EspHttpClient>();
if (!client->init(std::move(config))) {
onError("Failed to initialize client");
return -1;
}
if (!client->open()) {
onError("Failed to open connection");
return -1;
}
if (!client->fetchHeaders()) {
onError("Failed to get request headers");
return -1;
}
if (!client->isStatusCodeOk()) {
onError("Server response is not OK");
return -1;
}
auto bytes_left = client->getContentLength();
auto lock = file::getLock(downloadFilePath)->asScopedLock();
lock.lock();
auto file_exists = file::isFile(downloadFilePath);
auto* file_mode = file_exists ? "r+" : "w";
TT_LOG_I(TAG, "opening %s with mode %s", downloadFilePath.c_str(), file_mode);
auto* file = fopen(downloadFilePath.c_str(), file_mode);
if (file == nullptr) {
onError("Failed to open file");
return -1;
}
TT_LOG_I(TAG, "Writing %d bytes to %s", bytes_left, downloadFilePath.c_str());
char buffer[512];
while (bytes_left > 0) {
int data_read = client->read(buffer, 512);
if (data_read <= 0) {
fclose(file);
onError("Failed to read data");
return -1;
}
bytes_left -= data_read;
if (fwrite(buffer, 1, data_read, file) != data_read) {
fclose(file);
onError("Failed to write all bytes");
return -1;
}
}
fclose(file);
TT_LOG_I(TAG, "Downloaded %s to %s", url.c_str(), downloadFilePath.c_str());
onSuccess();
return 0;
});
#else
getMainDispatcher().dispatch([onError] {
onError("Not implemented");
});
#endif
}
}

View File

@ -14,7 +14,7 @@ bool HttpServer::startInternal() {
config.uri_match_fn = matchUri;
if (httpd_start(&server, &config) != ESP_OK) {
TT_LOG_E(TAG, "Failed to start http server on port %d", port);
TT_LOG_E(TAG, "Failed to start http server on port %lu", port);
return false;
}

View File

@ -1,11 +1,11 @@
#include <Tactility/network/HttpdReq.h>
#include <Tactility/Log.h>
#include <Tactility/StringUtils.h>
#include <Tactility/file/File.h>
#include <memory>
#include <ranges>
#include <sstream>
#include <Tactility/Log.h>
#include <Tactility/StringUtils.h>
#include <Tactility/file/File.h>
#ifdef ESP_PLATFORM

View File

@ -1,4 +1,5 @@
#include "Tactility/network/NtpPrivate.h"
#include <Tactility/network/NtpPrivate.h>
#include <Tactility/Preferences.h>
#ifdef ESP_PLATFORM
#include <Tactility/kernel/SystemEvents.h>
@ -7,19 +8,43 @@
#include <esp_sntp.h>
#endif
#define TAG "ntp"
namespace tt::network::ntp {
constexpr auto* TAG = "NTP";
static bool processedSyncEvent = false;
#ifdef ESP_PLATFORM
static void onTimeSynced(struct timeval* tv) {
void storeTimeInNvs() {
time_t now;
time(&now);
auto preferences = std::make_unique<Preferences>("time");
preferences->putInt64("syncTime", now);
TT_LOG_I(TAG, "Stored time %llu", now);
}
void setTimeFromNvs() {
auto preferences = std::make_unique<Preferences>("time");
time_t synced_time;
if (preferences->optInt64("syncTime", synced_time)) {
TT_LOG_I(TAG, "Restoring last known time to %llu", synced_time);
timeval get_nvs_time;
get_nvs_time.tv_sec = synced_time;
settimeofday(&get_nvs_time, nullptr);
}
}
static void onTimeSynced(timeval* tv) {
TT_LOG_I(TAG, "Time synced (%llu)", tv->tv_sec);
processedSyncEvent = true;
esp_netif_sntp_deinit();
storeTimeInNvs();
kernel::publishSystemEvent(kernel::SystemEvent::Time);
}
void init() {
esp_sntp_config_t config = ESP_NETIF_SNTP_DEFAULT_CONFIG("pool.ntp.org");
esp_sntp_config_t config = ESP_NETIF_SNTP_DEFAULT_CONFIG("time.cloudflare.com");
config.sync_cb = onTimeSynced;
esp_netif_sntp_init(&config);
}
@ -27,8 +52,13 @@ void init() {
#else
void init() {
processedSyncEvent = true;
}
#endif
bool isSynced() {
return processedSyncEvent;
}
}

View File

@ -209,7 +209,7 @@ esp_err_t DevelopmentService::handleAppUninstall(httpd_req_t* request) {
return ESP_FAIL;
}
if (!app::findAppById(id_key_pos->second)) {
if (!app::findAppManifestById(id_key_pos->second)) {
TT_LOG_I(TAG, "[200] /app/uninstall %s (app wasn't installed)", id_key_pos->second.c_str());
httpd_resp_send(request, nullptr, 0);
return ESP_OK;

View File

@ -43,7 +43,7 @@ static const char* appStateToString(app::State state) {
void LoaderService::onStartAppMessage(const std::string& id, app::LaunchId launchId, std::shared_ptr<const Bundle> parameters) {
TT_LOG_I(TAG, "Start by id %s", id.c_str());
auto app_manifest = app::findAppById(id);
auto app_manifest = app::findAppManifestById(id);
if (app_manifest == nullptr) {
TT_LOG_E(TAG, "App not found: %s", id.c_str());
return;

View File

@ -60,6 +60,13 @@ lv_obj_t* tt_lvgl_toolbar_add_switch_action(lv_obj_t* obj);
*/
lv_obj_t* tt_lvgl_toolbar_add_spinner_action(lv_obj_t* obj);
/**
* Remove all actions from the toolbar
* @param[in] obj the toolbar instance
*/
void tt_lvgl_toolbar_clear_actions(lv_obj_t* obj);
#ifdef __cplusplus
}
#endif

View File

@ -276,6 +276,7 @@ const esp_elfsym main_symbols[] {
ESP_ELFSYM_EXPORT(tt_lvgl_toolbar_add_text_button_action),
ESP_ELFSYM_EXPORT(tt_lvgl_toolbar_add_switch_action),
ESP_ELFSYM_EXPORT(tt_lvgl_toolbar_add_spinner_action),
ESP_ELFSYM_EXPORT(tt_lvgl_toolbar_clear_actions),
ESP_ELFSYM_EXPORT(tt_message_queue_alloc),
ESP_ELFSYM_EXPORT(tt_message_queue_free),
ESP_ELFSYM_EXPORT(tt_message_queue_put),

View File

@ -35,4 +35,8 @@ lv_obj_t* tt_lvgl_toolbar_add_spinner_action(lv_obj_t* obj) {
return tt::lvgl::toolbar_add_spinner_action(obj);
}
void tt_lvgl_toolbar_clear_actions(lv_obj_t* obj) {
tt::lvgl::toolbar_clear_actions(obj);
}
}

View File

@ -20,6 +20,7 @@ class Bundle final {
enum class Type {
Bool,
Int32,
Int64,
String,
};
@ -28,6 +29,7 @@ class Bundle final {
union {
bool value_bool;
int32_t value_int32;
int64_t value_int64;
};
std::string value_string;
} Value;
@ -44,18 +46,22 @@ public:
bool getBool(const std::string& key) const;
int32_t getInt32(const std::string& key) const;
int64_t getInt64(const std::string& key) const;
std::string getString(const std::string& key) const;
bool hasBool(const std::string& key) const;
bool hasInt32(const std::string& key) const;
bool hasInt64(const std::string& key) const;
bool hasString(const std::string& key) const;
bool optBool(const std::string& key, bool& out) const;
bool optInt32(const std::string& key, int32_t& out) const;
bool optInt64(const std::string& key, int64_t& out) const;
bool optString(const std::string& key, std::string& out) const;
void putBool(const std::string& key, bool value);
void putInt32(const std::string& key, int32_t value);
void putInt64(const std::string& key, int64_t value);
void putString(const std::string& key, const std::string& value);
};

View File

@ -51,6 +51,16 @@ void split(const std::string& input, const std::string& delimiter, std::function
*/
std::string join(const std::vector<std::string>& input, const std::string& delimiter);
/**
* Join a set of tokens into a single string, given a delimiter (separator).
* If the input is an empty list, the result will be an empty string.
* The delimeter is only placed inbetween tokens and not appended at the end of the resulting string.
*
* @param input the tokens to join together
* @param delimiter the separator to join with
*/
std::string join(const std::vector<const char*>& input, const std::string& delimiter);
/**
* Returns the lowercase value of a string.
* @warning This only works for strings with 1 byte per character

View File

@ -99,8 +99,18 @@ bool deleteDirectory(const std::string& path);
*/
std::string getChildPath(const std::string& basePath, const std::string& childPath);
/**
* Find the last part of the path. This can either be the filename or the deepest folder name.
* @param path an absolute or relative path
* @return the last segment of the specified path
*/
std::string getLastPathSegment(const std::string& path);
/**
* Find the first part of the path. This is either the root folder, or a file if the latter has no parent folder.
* @param path an absolute or relative path
* @return the first segment of the specified path
*/
std::string getFirstPathSegment(const std::string& path);
typedef int (*ScandirFilter)(const dirent*);

View File

@ -10,6 +10,10 @@ int32_t Bundle::getInt32(const std::string& key) const {
return this->entries.find(key)->second.value_int32;
}
int64_t Bundle::getInt64(const std::string& key) const {
return this->entries.find(key)->second.value_int64;
}
std::string Bundle::getString(const std::string& key) const {
return this->entries.find(key)->second.value_string;
}
@ -24,6 +28,11 @@ bool Bundle::hasInt32(const std::string& key) const {
return entry != std::end(this->entries) && entry->second.type == Type::Int32;
}
bool Bundle::hasInt64(const std::string& key) const {
auto entry = this->entries.find(key);
return entry != std::end(this->entries) && entry->second.type == Type::Int64;
}
bool Bundle::hasString(const std::string& key) const {
auto entry = this->entries.find(key);
return entry != std::end(this->entries) && entry->second.type == Type::String;
@ -49,6 +58,16 @@ bool Bundle::optInt32(const std::string& key, int32_t& out) const {
}
}
bool Bundle::optInt64(const std::string& key, int64_t& out) const {
auto entry = this->entries.find(key);
if (entry != std::end(this->entries) && entry->second.type == Type::Int64) {
out = entry->second.value_int32;
return true;
} else {
return false;
}
}
bool Bundle::optString(const std::string& key, std::string& out) const {
auto entry = this->entries.find(key);
if (entry != std::end(this->entries) && entry->second.type == Type::String) {
@ -75,6 +94,14 @@ void Bundle::putInt32(const std::string& key, int32_t value) {
};
}
void Bundle::putInt64(const std::string& key, int64_t value) {
this->entries[key] = {
.type = Type::Int64,
.value_int64 = value,
.value_string = ""
};
}
void Bundle::putString(const std::string& key, const std::string& value) {
this->entries[key] = {
.type = Type::String,

View File

@ -54,6 +54,28 @@ std::vector<std::string> split(const std::string&input, const std::string&delimi
return result;
}
std::string join(const std::vector<const char*>& input, const std::string& delimiter) {
std::stringstream stream;
size_t size = input.size();
if (size == 0) {
return "";
} else if (size == 1) {
return input.front();
} else {
auto iterator = input.begin();
while (iterator != input.end()) {
stream << *iterator;
iterator++;
if (iterator != input.end()) {
stream << delimiter;
}
}
}
return stream.str();
}
std::string join(const std::vector<std::string>& input, const std::string& delimiter) {
std::stringstream stream;
size_t size = input.size();

View File

@ -247,7 +247,7 @@ std::string getLastPathSegment(const std::string& path) {
if (index != std::string::npos) {
return path.substr(index + 1);
} else {
return "";
return path;
}
}

View File

@ -1,6 +1,7 @@
# Software defaults
# Increase stack size for WiFi (fixes crash after scan)
CONFIG_ESP_SYSTEM_EVENT_TASK_STACK_SIZE=3072
CONFIG_ESP_MAIN_TASK_STACK_SIZE=5120
CONFIG_LV_FONT_MONTSERRAT_14=y
CONFIG_LV_FONT_MONTSERRAT_18=y
CONFIG_LV_USE_USER_DATA=y
@ -28,6 +29,7 @@ CONFIG_WL_SECTOR_SIZE_512=y
CONFIG_WL_SECTOR_SIZE=512
CONFIG_WL_SECTOR_MODE_SAFE=y
CONFIG_WL_SECTOR_MODE=1
CONFIG_MBEDTLS_SSL_PROTO_TLS1_3=y
# Hardware: Main
CONFIG_PARTITION_TABLE_CUSTOM=y

View File

@ -1,6 +1,7 @@
# Software defaults
# Increase stack size for WiFi (fixes crash after scan)
CONFIG_ESP_SYSTEM_EVENT_TASK_STACK_SIZE=3072
CONFIG_ESP_MAIN_TASK_STACK_SIZE=5120
CONFIG_LV_FONT_MONTSERRAT_14=y
CONFIG_LV_FONT_MONTSERRAT_18=y
CONFIG_LV_USE_USER_DATA=y
@ -28,6 +29,7 @@ CONFIG_WL_SECTOR_SIZE_512=y
CONFIG_WL_SECTOR_SIZE=512
CONFIG_WL_SECTOR_MODE_SAFE=y
CONFIG_WL_SECTOR_MODE=1
CONFIG_MBEDTLS_SSL_PROTO_TLS1_3=y
# Hardware: Main
CONFIG_PARTITION_TABLE_CUSTOM=y

View File

@ -1,6 +1,7 @@
# Software defaults
# Increase stack size for WiFi (fixes crash after scan)
CONFIG_ESP_SYSTEM_EVENT_TASK_STACK_SIZE=3072
CONFIG_ESP_MAIN_TASK_STACK_SIZE=5120
CONFIG_LV_FONT_MONTSERRAT_14=y
CONFIG_LV_FONT_MONTSERRAT_18=y
CONFIG_LV_USE_USER_DATA=y
@ -28,6 +29,7 @@ CONFIG_WL_SECTOR_SIZE_512=y
CONFIG_WL_SECTOR_SIZE=512
CONFIG_WL_SECTOR_MODE_SAFE=y
CONFIG_WL_SECTOR_MODE=1
CONFIG_MBEDTLS_SSL_PROTO_TLS1_3=y
# Hardware: Main
CONFIG_PARTITION_TABLE_CUSTOM=y

View File

@ -1,6 +1,7 @@
# Software defaults
# Increase stack size for WiFi (fixes crash after scan)
CONFIG_ESP_SYSTEM_EVENT_TASK_STACK_SIZE=3072
CONFIG_ESP_MAIN_TASK_STACK_SIZE=5120
CONFIG_LV_FONT_MONTSERRAT_14=y
CONFIG_LV_FONT_MONTSERRAT_18=y
CONFIG_LV_USE_USER_DATA=y
@ -28,6 +29,7 @@ CONFIG_WL_SECTOR_SIZE_512=y
CONFIG_WL_SECTOR_SIZE=512
CONFIG_WL_SECTOR_MODE_SAFE=y
CONFIG_WL_SECTOR_MODE=1
CONFIG_MBEDTLS_SSL_PROTO_TLS1_3=y
# Hardware: Main
CONFIG_PARTITION_TABLE_CUSTOM=y

View File

@ -1,6 +1,7 @@
# Software defaults
# Increase stack size for WiFi (fixes crash after scan)
CONFIG_ESP_SYSTEM_EVENT_TASK_STACK_SIZE=3072
CONFIG_ESP_MAIN_TASK_STACK_SIZE=5120
CONFIG_LV_FONT_MONTSERRAT_14=y
CONFIG_LV_FONT_MONTSERRAT_18=y
CONFIG_LV_USE_USER_DATA=y
@ -28,6 +29,7 @@ CONFIG_WL_SECTOR_SIZE_512=y
CONFIG_WL_SECTOR_SIZE=512
CONFIG_WL_SECTOR_MODE_SAFE=y
CONFIG_WL_SECTOR_MODE=1
CONFIG_MBEDTLS_SSL_PROTO_TLS1_3=y
# Hardware: Main
CONFIG_PARTITION_TABLE_CUSTOM=y

View File

@ -1,5 +1,6 @@
# Software defaults
CONFIG_ESP_SYSTEM_EVENT_TASK_STACK_SIZE=3072
CONFIG_ESP_MAIN_TASK_STACK_SIZE=5120
CONFIG_LV_FONT_MONTSERRAT_14=y
CONFIG_LV_FONT_MONTSERRAT_18=y
CONFIG_LV_USE_USER_DATA=y

View File

@ -1,6 +1,7 @@
# Software defaults
# Increase stack size for WiFi (fixes crash after scan)
CONFIG_ESP_SYSTEM_EVENT_TASK_STACK_SIZE=3072
CONFIG_ESP_MAIN_TASK_STACK_SIZE=5120
CONFIG_LV_FONT_MONTSERRAT_14=y
CONFIG_LV_FONT_MONTSERRAT_18=y
CONFIG_LV_USE_USER_DATA=y
@ -28,6 +29,7 @@ CONFIG_WL_SECTOR_SIZE_512=y
CONFIG_WL_SECTOR_SIZE=512
CONFIG_WL_SECTOR_MODE_SAFE=y
CONFIG_WL_SECTOR_MODE=1
CONFIG_MBEDTLS_SSL_PROTO_TLS1_3=y
# Hardware: Main
CONFIG_PARTITION_TABLE_CUSTOM=y

View File

@ -1,6 +1,7 @@
# Software defaults
# Increase stack size for WiFi (fixes crash after scan)
CONFIG_ESP_SYSTEM_EVENT_TASK_STACK_SIZE=3072
CONFIG_ESP_MAIN_TASK_STACK_SIZE=5120
CONFIG_LV_FONT_MONTSERRAT_14=y
CONFIG_LV_FONT_MONTSERRAT_18=y
CONFIG_LV_USE_USER_DATA=y
@ -28,6 +29,7 @@ CONFIG_WL_SECTOR_SIZE_512=y
CONFIG_WL_SECTOR_SIZE=512
CONFIG_WL_SECTOR_MODE_SAFE=y
CONFIG_WL_SECTOR_MODE=1
CONFIG_MBEDTLS_SSL_PROTO_TLS1_3=y
# Hardware: Main
CONFIG_PARTITION_TABLE_CUSTOM=y

View File

@ -1,6 +1,7 @@
# Software defaults
# Increase stack size for WiFi (fixes crash after scan)
CONFIG_ESP_SYSTEM_EVENT_TASK_STACK_SIZE=3072
CONFIG_ESP_MAIN_TASK_STACK_SIZE=5120
CONFIG_LV_FONT_MONTSERRAT_14=y
CONFIG_LV_FONT_MONTSERRAT_18=y
CONFIG_LV_USE_USER_DATA=y
@ -28,6 +29,7 @@ CONFIG_WL_SECTOR_SIZE_512=y
CONFIG_WL_SECTOR_SIZE=512
CONFIG_WL_SECTOR_MODE_SAFE=y
CONFIG_WL_SECTOR_MODE=1
CONFIG_MBEDTLS_SSL_PROTO_TLS1_3=y
# Hardware: Main
CONFIG_PARTITION_TABLE_CUSTOM=y

View File

@ -1,6 +1,7 @@
# Software defaults
# Increase stack size for WiFi (fixes crash after scan)
CONFIG_ESP_SYSTEM_EVENT_TASK_STACK_SIZE=3072
CONFIG_ESP_MAIN_TASK_STACK_SIZE=5120
CONFIG_LV_FONT_MONTSERRAT_14=y
CONFIG_LV_FONT_MONTSERRAT_18=y
CONFIG_LV_USE_USER_DATA=y
@ -28,6 +29,7 @@ CONFIG_WL_SECTOR_SIZE_512=y
CONFIG_WL_SECTOR_SIZE=512
CONFIG_WL_SECTOR_MODE_SAFE=y
CONFIG_WL_SECTOR_MODE=1
CONFIG_MBEDTLS_SSL_PROTO_TLS1_3=y
# Hardware: Main
CONFIG_PARTITION_TABLE_CUSTOM=y

View File

@ -1,6 +1,7 @@
# Software defaults
# Increase stack size for WiFi (fixes crash after scan)
CONFIG_ESP_SYSTEM_EVENT_TASK_STACK_SIZE=3072
CONFIG_ESP_MAIN_TASK_STACK_SIZE=5120
CONFIG_LV_FONT_MONTSERRAT_14=y
CONFIG_LV_FONT_MONTSERRAT_18=y
CONFIG_LV_USE_USER_DATA=y
@ -28,6 +29,7 @@ CONFIG_WL_SECTOR_SIZE_512=y
CONFIG_WL_SECTOR_SIZE=512
CONFIG_WL_SECTOR_MODE_SAFE=y
CONFIG_WL_SECTOR_MODE=1
CONFIG_MBEDTLS_SSL_PROTO_TLS1_3=y
# Hardware: Main
CONFIG_PARTITION_TABLE_CUSTOM=y

View File

@ -1,6 +1,7 @@
# Software defaults
# Increase stack size for WiFi (fixes crash after scan)
CONFIG_ESP_SYSTEM_EVENT_TASK_STACK_SIZE=3072
CONFIG_ESP_MAIN_TASK_STACK_SIZE=5120
CONFIG_LV_FONT_MONTSERRAT_14=y
CONFIG_LV_FONT_MONTSERRAT_18=y
CONFIG_LV_USE_USER_DATA=y
@ -28,6 +29,7 @@ CONFIG_WL_SECTOR_SIZE_512=y
CONFIG_WL_SECTOR_SIZE=512
CONFIG_WL_SECTOR_MODE_SAFE=y
CONFIG_WL_SECTOR_MODE=1
CONFIG_MBEDTLS_SSL_PROTO_TLS1_3=y
# Hardware: Main
CONFIG_PARTITION_TABLE_CUSTOM=y

View File

@ -1,6 +1,7 @@
# Software defaults
# Increase stack size for WiFi (fixes crash after scan)
CONFIG_ESP_SYSTEM_EVENT_TASK_STACK_SIZE=3072
CONFIG_ESP_MAIN_TASK_STACK_SIZE=5120
CONFIG_LV_FONT_MONTSERRAT_14=y
CONFIG_LV_FONT_MONTSERRAT_18=y
CONFIG_LV_USE_USER_DATA=y
@ -28,6 +29,7 @@ CONFIG_WL_SECTOR_SIZE_512=y
CONFIG_WL_SECTOR_SIZE=512
CONFIG_WL_SECTOR_MODE_SAFE=y
CONFIG_WL_SECTOR_MODE=1
CONFIG_MBEDTLS_SSL_PROTO_TLS1_3=y
# Hardware: Main
CONFIG_PARTITION_TABLE_CUSTOM=y

View File

@ -1,6 +1,7 @@
# Software defaults
# Increase stack size for WiFi (fixes crash after scan)
CONFIG_ESP_SYSTEM_EVENT_TASK_STACK_SIZE=3072
CONFIG_ESP_MAIN_TASK_STACK_SIZE=5120
CONFIG_LV_FONT_MONTSERRAT_14=y
CONFIG_LV_FONT_MONTSERRAT_18=y
CONFIG_LV_USE_USER_DATA=y
@ -28,6 +29,7 @@ CONFIG_WL_SECTOR_SIZE_512=y
CONFIG_WL_SECTOR_SIZE=512
CONFIG_WL_SECTOR_MODE_SAFE=y
CONFIG_WL_SECTOR_MODE=1
CONFIG_MBEDTLS_SSL_PROTO_TLS1_3=y
# Hardware: Main
CONFIG_PARTITION_TABLE_CUSTOM=y

View File

@ -1,6 +1,7 @@
# Software defaults
# Increase stack size for WiFi (fixes crash after scan)
CONFIG_ESP_SYSTEM_EVENT_TASK_STACK_SIZE=3072
CONFIG_ESP_MAIN_TASK_STACK_SIZE=5120
CONFIG_LV_FONT_MONTSERRAT_14=y
CONFIG_LV_FONT_MONTSERRAT_18=y
CONFIG_LV_USE_USER_DATA=y
@ -28,6 +29,7 @@ CONFIG_WL_SECTOR_SIZE_512=y
CONFIG_WL_SECTOR_SIZE=512
CONFIG_WL_SECTOR_MODE_SAFE=y
CONFIG_WL_SECTOR_MODE=1
CONFIG_MBEDTLS_SSL_PROTO_TLS1_3=y
# Hardware: Main
CONFIG_PARTITION_TABLE_CUSTOM=y

View File

@ -1,6 +1,7 @@
# Software defaults
# Increase stack size for WiFi (fixes crash after scan)
CONFIG_ESP_SYSTEM_EVENT_TASK_STACK_SIZE=3072
CONFIG_ESP_MAIN_TASK_STACK_SIZE=5120
CONFIG_LV_FONT_MONTSERRAT_14=y
CONFIG_LV_FONT_MONTSERRAT_18=y
CONFIG_LV_USE_USER_DATA=y
@ -28,6 +29,7 @@ CONFIG_WL_SECTOR_SIZE_512=y
CONFIG_WL_SECTOR_SIZE=512
CONFIG_WL_SECTOR_MODE_SAFE=y
CONFIG_WL_SECTOR_MODE=1
CONFIG_MBEDTLS_SSL_PROTO_TLS1_3=y
# Hardware: Main
CONFIG_PARTITION_TABLE_CUSTOM=y

View File

@ -1,6 +1,7 @@
# Software defaults
# Increase stack size for WiFi (fixes crash after scan)
CONFIG_ESP_SYSTEM_EVENT_TASK_STACK_SIZE=3072
CONFIG_ESP_MAIN_TASK_STACK_SIZE=5120
CONFIG_LV_FONT_MONTSERRAT_14=y
CONFIG_LV_FONT_MONTSERRAT_18=y
CONFIG_LV_USE_USER_DATA=y
@ -28,6 +29,7 @@ CONFIG_WL_SECTOR_SIZE_512=y
CONFIG_WL_SECTOR_SIZE=512
CONFIG_WL_SECTOR_MODE_SAFE=y
CONFIG_WL_SECTOR_MODE=1
CONFIG_MBEDTLS_SSL_PROTO_TLS1_3=y
# Hardware: Main
CONFIG_PARTITION_TABLE_CUSTOM=y

View File

@ -1,6 +1,7 @@
# Software defaults
# Increase stack size for WiFi (fixes crash after scan)
CONFIG_ESP_SYSTEM_EVENT_TASK_STACK_SIZE=3072
CONFIG_ESP_MAIN_TASK_STACK_SIZE=5120
CONFIG_LV_FONT_MONTSERRAT_14=y
CONFIG_LV_FONT_MONTSERRAT_18=y
CONFIG_LV_USE_USER_DATA=y
@ -28,6 +29,7 @@ CONFIG_WL_SECTOR_SIZE_512=y
CONFIG_WL_SECTOR_SIZE=512
CONFIG_WL_SECTOR_MODE_SAFE=y
CONFIG_WL_SECTOR_MODE=1
CONFIG_MBEDTLS_SSL_PROTO_TLS1_3=y
# Hardware: Main
CONFIG_PARTITION_TABLE_CUSTOM=y

View File

@ -1,6 +1,7 @@
# Software defaults
# Increase stack size for WiFi (fixes crash after scan)
CONFIG_ESP_SYSTEM_EVENT_TASK_STACK_SIZE=3072
CONFIG_ESP_MAIN_TASK_STACK_SIZE=5120
CONFIG_LV_FONT_MONTSERRAT_14=y
CONFIG_LV_FONT_MONTSERRAT_18=y
CONFIG_LV_USE_USER_DATA=y
@ -28,6 +29,7 @@ CONFIG_WL_SECTOR_SIZE_512=y
CONFIG_WL_SECTOR_SIZE=512
CONFIG_WL_SECTOR_MODE_SAFE=y
CONFIG_WL_SECTOR_MODE=1
CONFIG_MBEDTLS_SSL_PROTO_TLS1_3=y
# Hardware: Main
CONFIG_PARTITION_TABLE_CUSTOM=y

View File

@ -1,6 +1,7 @@
# Software defaults
# Increase stack size for WiFi (fixes crash after scan)
CONFIG_ESP_SYSTEM_EVENT_TASK_STACK_SIZE=3072
CONFIG_ESP_MAIN_TASK_STACK_SIZE=5120
CONFIG_LV_FONT_MONTSERRAT_14=y
CONFIG_LV_FONT_MONTSERRAT_18=y
CONFIG_LV_USE_USER_DATA=y
@ -28,6 +29,7 @@ CONFIG_WL_SECTOR_SIZE_512=y
CONFIG_WL_SECTOR_SIZE=512
CONFIG_WL_SECTOR_MODE_SAFE=y
CONFIG_WL_SECTOR_MODE=1
CONFIG_MBEDTLS_SSL_PROTO_TLS1_3=y
# Hardware: Main
CONFIG_PARTITION_TABLE_CUSTOM=y

View File

@ -1,6 +1,7 @@
# Software defaults
# Increase stack size for WiFi (fixes crash after scan)
CONFIG_ESP_SYSTEM_EVENT_TASK_STACK_SIZE=3072
CONFIG_ESP_MAIN_TASK_STACK_SIZE=5120
CONFIG_LV_FONT_MONTSERRAT_14=y
CONFIG_LV_FONT_MONTSERRAT_18=y
CONFIG_LV_USE_USER_DATA=y
@ -28,6 +29,7 @@ CONFIG_WL_SECTOR_SIZE_512=y
CONFIG_WL_SECTOR_SIZE=512
CONFIG_WL_SECTOR_MODE_SAFE=y
CONFIG_WL_SECTOR_MODE=1
CONFIG_MBEDTLS_SSL_PROTO_TLS1_3=y
# Hardware: Main
CONFIG_PARTITION_TABLE_CUSTOM=y

View File

@ -1,6 +1,7 @@
# Software defaults
# Increase stack size for WiFi (fixes crash after scan)
CONFIG_ESP_SYSTEM_EVENT_TASK_STACK_SIZE=3072
CONFIG_ESP_MAIN_TASK_STACK_SIZE=5120
CONFIG_LV_FONT_MONTSERRAT_14=y
CONFIG_LV_FONT_MONTSERRAT_18=y
CONFIG_LV_USE_USER_DATA=y
@ -28,6 +29,7 @@ CONFIG_WL_SECTOR_SIZE_512=y
CONFIG_WL_SECTOR_SIZE=512
CONFIG_WL_SECTOR_MODE_SAFE=y
CONFIG_WL_SECTOR_MODE=1
CONFIG_MBEDTLS_SSL_PROTO_TLS1_3=y
# Hardware: Main
CONFIG_PARTITION_TABLE_CUSTOM=y

View File

@ -1,6 +1,7 @@
# Software defaults
# Increase stack size for WiFi (fixes crash after scan)
CONFIG_ESP_SYSTEM_EVENT_TASK_STACK_SIZE=3072
CONFIG_ESP_MAIN_TASK_STACK_SIZE=5120
CONFIG_LV_FONT_MONTSERRAT_14=y
CONFIG_LV_FONT_MONTSERRAT_18=y
CONFIG_LV_USE_USER_DATA=y
@ -28,6 +29,7 @@ CONFIG_WL_SECTOR_SIZE_512=y
CONFIG_WL_SECTOR_SIZE=512
CONFIG_WL_SECTOR_MODE_SAFE=y
CONFIG_WL_SECTOR_MODE=1
CONFIG_MBEDTLS_SSL_PROTO_TLS1_3=y
# Hardware: Main
CONFIG_PARTITION_TABLE_CUSTOM=y

View File

@ -1,6 +1,7 @@
# Software defaults
# Increase stack size for WiFi (fixes crash after scan)
CONFIG_ESP_SYSTEM_EVENT_TASK_STACK_SIZE=3072
CONFIG_ESP_MAIN_TASK_STACK_SIZE=5120
CONFIG_LV_FONT_MONTSERRAT_14=y
CONFIG_LV_FONT_MONTSERRAT_18=y
CONFIG_LV_USE_USER_DATA=y
@ -28,6 +29,7 @@ CONFIG_WL_SECTOR_SIZE_512=y
CONFIG_WL_SECTOR_SIZE=512
CONFIG_WL_SECTOR_MODE_SAFE=y
CONFIG_WL_SECTOR_MODE=1
CONFIG_MBEDTLS_SSL_PROTO_TLS1_3=y
# Hardware: Main
CONFIG_PARTITION_TABLE_CUSTOM=y

View File

@ -1,6 +1,7 @@
# Software defaults
# Increase stack size for WiFi (fixes crash after scan)
CONFIG_ESP_SYSTEM_EVENT_TASK_STACK_SIZE=3072
CONFIG_ESP_MAIN_TASK_STACK_SIZE=5120
CONFIG_LV_FONT_MONTSERRAT_14=y
CONFIG_LV_FONT_MONTSERRAT_18=y
CONFIG_LV_USE_USER_DATA=y
@ -28,6 +29,7 @@ CONFIG_WL_SECTOR_SIZE_512=y
CONFIG_WL_SECTOR_SIZE=512
CONFIG_WL_SECTOR_MODE_SAFE=y
CONFIG_WL_SECTOR_MODE=1
CONFIG_MBEDTLS_SSL_PROTO_TLS1_3=y
# Hardware: Main
CONFIG_PARTITION_TABLE_CUSTOM=y

View File

@ -1,6 +1,7 @@
# Software defaults
# Increase stack size for WiFi (fixes crash after scan)
CONFIG_ESP_SYSTEM_EVENT_TASK_STACK_SIZE=3072
CONFIG_ESP_MAIN_TASK_STACK_SIZE=5120
CONFIG_LV_FONT_MONTSERRAT_14=y
CONFIG_LV_FONT_MONTSERRAT_18=y
CONFIG_LV_USE_USER_DATA=y
@ -28,6 +29,7 @@ CONFIG_WL_SECTOR_SIZE_512=y
CONFIG_WL_SECTOR_SIZE=512
CONFIG_WL_SECTOR_MODE_SAFE=y
CONFIG_WL_SECTOR_MODE=1
CONFIG_MBEDTLS_SSL_PROTO_TLS1_3=y
# Hardware: Main
CONFIG_PARTITION_TABLE_CUSTOM=y

View File

@ -1,6 +1,7 @@
# Software defaults
# Increase stack size for WiFi (fixes crash after scan)
CONFIG_ESP_SYSTEM_EVENT_TASK_STACK_SIZE=3072
CONFIG_ESP_MAIN_TASK_STACK_SIZE=5120
CONFIG_LV_FONT_MONTSERRAT_14=y
CONFIG_LV_FONT_MONTSERRAT_18=y
CONFIG_LV_USE_USER_DATA=y
@ -28,6 +29,7 @@ CONFIG_WL_SECTOR_SIZE_512=y
CONFIG_WL_SECTOR_SIZE=512
CONFIG_WL_SECTOR_MODE_SAFE=y
CONFIG_WL_SECTOR_MODE=1
CONFIG_MBEDTLS_SSL_PROTO_TLS1_3=y
# Hardware: Main
CONFIG_PARTITION_TABLE_CUSTOM=y

View File

@ -1,6 +1,7 @@
# Software defaults
# Increase stack size for WiFi (fixes crash after scan)
CONFIG_ESP_SYSTEM_EVENT_TASK_STACK_SIZE=3072
CONFIG_ESP_MAIN_TASK_STACK_SIZE=5120
CONFIG_LV_FONT_MONTSERRAT_14=y
CONFIG_LV_FONT_MONTSERRAT_18=y
CONFIG_LV_USE_USER_DATA=y
@ -28,6 +29,7 @@ CONFIG_WL_SECTOR_SIZE_512=y
CONFIG_WL_SECTOR_SIZE=512
CONFIG_WL_SECTOR_MODE_SAFE=y
CONFIG_WL_SECTOR_MODE=1
CONFIG_MBEDTLS_SSL_PROTO_TLS1_3=y
# Hardware: Main
CONFIG_PARTITION_TABLE_CUSTOM=y

View File

@ -1,6 +1,7 @@
# Software defaults
# Increase stack size for WiFi (fixes crash after scan)
CONFIG_ESP_SYSTEM_EVENT_TASK_STACK_SIZE=3072
CONFIG_ESP_MAIN_TASK_STACK_SIZE=5120
CONFIG_LV_FONT_MONTSERRAT_14=y
CONFIG_LV_FONT_MONTSERRAT_18=y
CONFIG_LV_USE_USER_DATA=y
@ -28,6 +29,7 @@ CONFIG_WL_SECTOR_SIZE_512=y
CONFIG_WL_SECTOR_SIZE=512
CONFIG_WL_SECTOR_MODE_SAFE=y
CONFIG_WL_SECTOR_MODE=1
CONFIG_MBEDTLS_SSL_PROTO_TLS1_3=y
# Hardware defaults
CONFIG_PARTITION_TABLE_CUSTOM=y