Merge develop into main (#327)
## New features - Implemented support for app packaging in firmware and `tactility.py`: load `.app` files instead of `.elf` files. Install apps remotely or via `FileBrowser`. - Ensure headless mode works: all services that require LVGL can deal with the absence of a display - Service `onStart()` is now allowed to fail (return `bool` result) - Added and improved various file-related helper functions ## Improvements - Completely revamped the SystemInfo app UI - Improved Calculator UI of internal and external variant - Fix Chat UI and removed the emoji buttons for now - Fix for toolbar bottom padding issue in all apps ## Fixes - Fix for allowing recursive locking for certain SPI SD cards & more
This commit is contained in:
parent
068600f98c
commit
84049658db
3
.gitmodules
vendored
3
.gitmodules
vendored
@ -10,3 +10,6 @@
|
||||
[submodule "Libraries/SDL"]
|
||||
path = Libraries/SDL
|
||||
url = https://github.com/libsdl-org/SDL.git
|
||||
[submodule "Libraries/minitar/minitar"]
|
||||
path = Libraries/minitar/minitar
|
||||
url = git@github.com:gabscoarnec/minitar.git
|
||||
|
||||
@ -16,7 +16,7 @@ std::shared_ptr<SdCardDevice> createSdCard() {
|
||||
GPIO_NUM_NC,
|
||||
GPIO_NUM_NC,
|
||||
SdCardDevice::MountBehaviour::AtBoot,
|
||||
std::make_shared<tt::Mutex>(),
|
||||
std::make_shared<tt::Mutex>(tt::Mutex::Type::Recursive),
|
||||
std::vector<gpio_num_t>(),
|
||||
SDCARD_SPI_HOST
|
||||
);
|
||||
|
||||
@ -11,7 +11,7 @@ std::shared_ptr<SdCardDevice> createSdCard() {
|
||||
GPIO_NUM_NC,
|
||||
GPIO_NUM_NC,
|
||||
SdCardDevice::MountBehaviour::AtBoot,
|
||||
std::make_shared<tt::Mutex>(),
|
||||
std::make_shared<tt::Mutex>(tt::Mutex::Type::Recursive),
|
||||
std::vector<gpio_num_t>(),
|
||||
SPI3_HOST
|
||||
);
|
||||
|
||||
@ -15,7 +15,7 @@ std::shared_ptr<SdCardDevice> createSdCard() {
|
||||
GPIO_NUM_NC,
|
||||
GPIO_NUM_NC,
|
||||
SdCardDevice::MountBehaviour::AtBoot,
|
||||
std::make_shared<tt::Mutex>(),
|
||||
std::make_shared<tt::Mutex>(tt::Mutex::Type::Recursive),
|
||||
std::vector<gpio_num_t>(),
|
||||
SDCARD_SPI_HOST
|
||||
);
|
||||
|
||||
@ -10,7 +10,7 @@ std::shared_ptr<SdCardDevice> createSdCard() {
|
||||
GPIO_NUM_NC,
|
||||
GPIO_NUM_NC,
|
||||
SdCardDevice::MountBehaviour::AtBoot,
|
||||
std::make_shared<tt::Mutex>(),
|
||||
std::make_shared<tt::Mutex>(tt::Mutex::Type::Recursive),
|
||||
std::vector<gpio_num_t>(),
|
||||
SPI3_HOST
|
||||
);
|
||||
|
||||
@ -15,7 +15,7 @@ std::shared_ptr<SdCardDevice> createSdCard() {
|
||||
GPIO_NUM_NC,
|
||||
GPIO_NUM_NC,
|
||||
SdCardDevice::MountBehaviour::AtBoot,
|
||||
std::make_shared<tt::Mutex>(),
|
||||
std::make_shared<tt::Mutex>(tt::Mutex::Type::Recursive),
|
||||
std::vector<gpio_num_t>(),
|
||||
SDCARD_SPI_HOST
|
||||
);
|
||||
|
||||
@ -12,7 +12,7 @@ std::shared_ptr<SdCardDevice> createSdCard() {
|
||||
GPIO_NUM_NC,
|
||||
GPIO_NUM_NC,
|
||||
SdCardDevice::MountBehaviour::AtBoot,
|
||||
std::make_shared<tt::Mutex>(),
|
||||
std::make_shared<tt::Mutex>(tt::Mutex::Type::Recursive),
|
||||
std::vector<gpio_num_t>(),
|
||||
SPI2_HOST
|
||||
);
|
||||
|
||||
@ -16,7 +16,7 @@ public:
|
||||
|
||||
SimulatorSdCard() : SdCardDevice(MountBehaviour::AtBoot),
|
||||
state(State::Unmounted),
|
||||
lock(std::make_shared<tt::Mutex>())
|
||||
lock(std::make_shared<tt::Mutex>(tt::Mutex::Type::Recursive))
|
||||
{}
|
||||
|
||||
std::string getName() const override { return "Mock SD Card"; }
|
||||
|
||||
@ -41,6 +41,7 @@ if (DEFINED ENV{ESP_IDF_VERSION})
|
||||
"Libraries/elf_loader"
|
||||
"Libraries/lvgl"
|
||||
"Libraries/lv_screenshot"
|
||||
"Libraries/minitar"
|
||||
"Libraries/minmea"
|
||||
"Libraries/QRCode"
|
||||
)
|
||||
@ -78,6 +79,7 @@ if (NOT DEFINED ENV{ESP_IDF_VERSION})
|
||||
add_subdirectory(Libraries/FreeRTOS-Kernel)
|
||||
add_subdirectory(Libraries/lv_screenshot)
|
||||
add_subdirectory(Libraries/QRCode)
|
||||
add_subdirectory(Libraries/minitar)
|
||||
add_subdirectory(Libraries/minmea)
|
||||
target_compile_definitions(freertos_kernel PUBLIC "projCOVERAGE_TEST=0")
|
||||
target_include_directories(freertos_kernel
|
||||
|
||||
@ -2,7 +2,6 @@
|
||||
|
||||
## Higher Priority
|
||||
|
||||
- System Info should show the version of Tactility
|
||||
- Fix Development service: when no SD card is present, the app fails to install. Consider installing to `/data`
|
||||
Note: Change app install to "transfer file" functionality. We can have a proper install when we have app packaging.
|
||||
Note: Consider installation path option in interface
|
||||
@ -11,34 +10,30 @@
|
||||
Create some kind of "intent" handler like on Android.
|
||||
The intent can have an action (e.g. view), a URL and an optional bundle.
|
||||
The manifest can provide the intent handler
|
||||
- Bug: GraphicsDemo should check if display supports the DisplayDriver interface (and same for touch) and show an AlertDialog error if there's a problem
|
||||
- Update ILI934x to v2.0.1
|
||||
- App packaging
|
||||
- Create an "app install paths" settings app to add/remove paths.
|
||||
Scan these paths on startup.
|
||||
Make the AppList use the scan results.
|
||||
- Apps with update timer should check `lvgl::isStarted()`
|
||||
- Apps with update timer in onCreate() should check `lvgl::isStarted()`
|
||||
- CrowPanel Basic 3.5": check why GraphicsDemo fails
|
||||
- CrowPanel Basic 3.5": check why System Info doesn't show storage info
|
||||
- Update to LVGL v9.3 stable
|
||||
- Files app: delete folder recursively
|
||||
|
||||
## Medium Priority
|
||||
|
||||
- Statusbar icon that shows low/critical memory warnings
|
||||
- Make WiFi setup app that starts an access point and hosts a webpage to set up the device.
|
||||
This will be useful for devices without a screen, a small screen or a non-touch screen.
|
||||
- Unify the way displays are dimmed. Some implementations turn off the display when it's fully dimmed. Make this a separate functionality.
|
||||
- Try out ILI9342 https://github.com/jbrilha/esp_lcd_ili9342
|
||||
- All drivers (e.g. display, touch, etc.) should call stop() in their destructor, or at least assert that they should not be running.
|
||||
- Create different partition files for different ESP flash size targets (N4, N8, N16, N32)
|
||||
Consider a dev variant for quick flashing.
|
||||
- Bug: Turn on WiFi (when testing it wasn't connected/connecting - just active). Open chat. Observe crash.
|
||||
|
||||
## Lower Priority
|
||||
|
||||
- Localize all apps
|
||||
- Support hot-plugging SD card (note: this is not possible if they require the CS pin hack)
|
||||
- Create more unit tests for `tactility`
|
||||
- Explore LVGL9's FreeRTOS functionality
|
||||
- CrashHandler: use "corrupted" flag
|
||||
- CrashHandler: process other types of crashes (WDT?)
|
||||
- Add a Keyboard setting in `keyboard.properties` to override the behaviour of soft keyboard hiding (e.g. keyboard hardware is present, but the user wants to use a soft keyboard)
|
||||
- Use GPS time to set/update the current time
|
||||
- Fix bug in T-Deck/etc: esp_lvgl_port settings has a large stack size (~9kB) to fix an issue where the T-Deck would get a stackoverflow. This sometimes happens when WiFi is auto-enabled and you open the app while it is still connecting.
|
||||
- Consider using non_null (either via MS GSL, or custom)
|
||||
@ -50,21 +45,20 @@
|
||||
- T-Deck: Use trackball as input device (with optional mouse functionality for LVGL)
|
||||
- Show a warning screen if firmware encryption or secure boot are off when saving WiFi credentials.
|
||||
- Remove flex_flow from app_container in Gui.cpp
|
||||
- Files app: copy/cut/paste actions
|
||||
|
||||
# Nice-to-haves
|
||||
|
||||
- Considering the lack of callstack debugging for external apps: allow for some debugging to be exposed during a device crash. Apps could report their state (e.g. an integer value) which can be stored during app operation and retrieve after crash. The same can be done for various OS apps and states. We can keep an array of these numbers to keep track of the last X states, to get an idea of what's going on.
|
||||
- Give external app a different icon. Allow an external app to update their id, icon, type and name once they are running (and persist that info?). Loader will need to be able to find app by (external) location.
|
||||
- Audio player app
|
||||
- Audio recording app
|
||||
- OTA updates
|
||||
- T-Deck Plus: Create separate board config?
|
||||
- Support for displays with different DPI. Consider the layer-based system like on Android.
|
||||
- If present, use LED to show boot/wifi status
|
||||
- Capacity based on voltage: estimation for various devices uses a linear voltage curve, but it should use some sort of battery discharge curve.
|
||||
- Statusbar widget to show how much memory is in use?
|
||||
- Capacity based on voltage: estimation for various devices uses a linear voltage curve, but it should use a battery discharge curve.
|
||||
- Wrapper for lvgl slider widget that shows "+" and "-" buttons, and also the value in a label.
|
||||
- Files app: copy/paste actions
|
||||
Note: consider Spinbox
|
||||
- On crash, try to save the current log to flash or SD card? (this is risky, though, so ask in Discord first)
|
||||
- Support more than 1 hardware keyboard (see lvgl::hardware_keyboard_set_indev()). LVGL init currently calls keyboard init, but that part should probably be done from the KeyboardDevice base class.
|
||||
|
||||
|
||||
@ -25,4 +25,4 @@
|
||||
|
||||
1. Mention on Discord
|
||||
2. Consider notifying vendors/stakeholders
|
||||
3. Remove dev versions in `sdk.json`from [TactilityTool](https://github.com/ByteWelder/TactilityTool) and upload it to [CDN](https://dash.cloudflare.com)
|
||||
3. Update SDK updates to CDN at [TactilityTool](https://github.com/ByteWelder/TactilityTool) and upload it to [CDN](https://dash.cloudflare.com)
|
||||
@ -145,6 +145,7 @@ void Calculator::resetCalculator() {
|
||||
void Calculator::onShow(AppHandle appHandle, lv_obj_t* parent) {
|
||||
lv_obj_remove_flag(parent, LV_OBJ_FLAG_SCROLLABLE);
|
||||
lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN);
|
||||
lv_obj_set_style_pad_row(parent, 0, LV_STATE_DEFAULT);
|
||||
|
||||
lv_obj_t* toolbar = tt_lvgl_toolbar_create_for_app(parent, appHandle);
|
||||
lv_obj_align(toolbar, LV_ALIGN_TOP_MID, 0, 0);
|
||||
@ -187,10 +188,9 @@ void Calculator::onShow(AppHandle appHandle, lv_obj_t* parent) {
|
||||
lv_obj_set_style_pad_all(buttonmatrix, 5, LV_PART_MAIN);
|
||||
lv_obj_set_style_pad_row(buttonmatrix, 10, LV_PART_MAIN);
|
||||
lv_obj_set_style_pad_column(buttonmatrix, 5, LV_PART_MAIN);
|
||||
lv_obj_set_style_border_width(buttonmatrix, 0, LV_PART_MAIN);
|
||||
lv_obj_set_style_border_width(buttonmatrix, 2, LV_PART_MAIN);
|
||||
lv_obj_set_style_bg_color(buttonmatrix, lv_palette_main(LV_PALETTE_BLUE), LV_PART_ITEMS);
|
||||
|
||||
lv_obj_set_style_border_width(buttonmatrix, 0, LV_PART_MAIN);
|
||||
if (lv_display_get_horizontal_resolution(nullptr) <= 240 || lv_display_get_vertical_resolution(nullptr) <= 240) {
|
||||
//small screens
|
||||
lv_obj_set_size(buttonmatrix, lv_pct(100), lv_pct(60));
|
||||
|
||||
13
ExternalApps/Calculator/manifest.properties
Normal file
13
ExternalApps/Calculator/manifest.properties
Normal file
@ -0,0 +1,13 @@
|
||||
[manifest]
|
||||
version=0.1
|
||||
[target]
|
||||
sdk=0.5.0
|
||||
platforms=esp32,esp32s3
|
||||
[app]
|
||||
id=com.bytewelder.calculator
|
||||
version=0.1.0
|
||||
name=Calculator
|
||||
description=Math is cool
|
||||
[author]
|
||||
name=ByteWelder
|
||||
website=https://bytewelder.com
|
||||
@ -1,2 +0,0 @@
|
||||
[sdk]
|
||||
version = 0.5.0
|
||||
@ -8,22 +8,19 @@ import subprocess
|
||||
import time
|
||||
import urllib.request
|
||||
import zipfile
|
||||
|
||||
import requests
|
||||
import tarfile
|
||||
import shutil
|
||||
import configparser
|
||||
|
||||
# Targetable platforms that represent a specific hardware target
|
||||
platform_targets = ["esp32", "esp32s3"]
|
||||
# All valid platform commandline arguments
|
||||
platform_arguments = platform_targets.copy()
|
||||
platform_arguments.append("all")
|
||||
ttbuild_path = ".tactility"
|
||||
ttbuild_version = "1.2.1"
|
||||
ttbuild_properties_file = "tactility.properties"
|
||||
ttbuild_version = "2.0.0"
|
||||
ttbuild_cdn = "https://cdn.tactility.one"
|
||||
ttbuild_sdk_json_validity = 3600 # seconds
|
||||
ttport = 6666
|
||||
verbose = False
|
||||
use_local_sdk = False
|
||||
valid_platforms = ["esp32", "esp32s3"]
|
||||
|
||||
spinner_pattern = [
|
||||
"⠋",
|
||||
@ -57,21 +54,24 @@ def print_help():
|
||||
print("Usage: python tactility.py [action] [options]")
|
||||
print("")
|
||||
print("Actions:")
|
||||
print(" build [esp32,esp32s3,all,local] Build the app for the specified platform")
|
||||
print(" build [esp32,esp32s3] Build the app. Optionally specify a platform.")
|
||||
print(" esp32: ESP32")
|
||||
print(" esp32s3: ESP32 S3")
|
||||
print(" all: all supported ESP platforms")
|
||||
print(" clean Clean the build folders")
|
||||
print(" clearcache Clear the SDK cache")
|
||||
print(" updateself Update this tool")
|
||||
print(" run [ip] [app id] Run an application")
|
||||
print(" install [ip] [esp32,esp32s3] Install an application")
|
||||
print(" clean Clean the build folders")
|
||||
print(" clearcache Clear the SDK cache")
|
||||
print(" updateself Update this tool")
|
||||
print(" run [ip] Run an application")
|
||||
print(" install [ip] Install an application")
|
||||
print(" bir [ip] [esp32,esp32s3] Build, install then run. Optionally specify a platform.")
|
||||
print(" brrr [ip] [esp32,esp32s3] Functionally the same as \"bir\", but \"app goes brrr\" meme variant.")
|
||||
print("")
|
||||
print("Options:")
|
||||
print(" --help Show this commandline info")
|
||||
print(" --local-sdk Use SDK specified by environment variable TACTILITY_SDK_PATH")
|
||||
print(" --skip-build Run everything except the idf.py/CMake commands")
|
||||
print(" --verbose Show extra console output")
|
||||
print(" --help Show this commandline info")
|
||||
print(" --local-sdk Use SDK specified by environment variable TACTILITY_SDK_PATH")
|
||||
print(" --skip-build Run everything except the idf.py/CMake commands")
|
||||
print(" --verbose Show extra console output")
|
||||
|
||||
# region Core
|
||||
|
||||
def download_file(url, filepath):
|
||||
global verbose
|
||||
@ -108,25 +108,19 @@ def exit_with_error(message):
|
||||
def get_url(ip, path):
|
||||
return f"http://{ip}:{ttport}{path}"
|
||||
|
||||
def is_valid_platform_name(name):
|
||||
global platform_arguments
|
||||
return name in platform_arguments
|
||||
def read_properties_file(path):
|
||||
config = configparser.RawConfigParser()
|
||||
config.read(path)
|
||||
return config
|
||||
|
||||
def validate_environment():
|
||||
global ttbuild_properties_file, use_local_sdk
|
||||
if os.environ.get("IDF_PATH") is None:
|
||||
exit_with_error("Cannot find the Espressif IDF SDK. Ensure it is installed and that it is activated via $PATH_TO_IDF_SDK/export.sh")
|
||||
if not os.path.exists(ttbuild_properties_file):
|
||||
exit_with_error(f"{ttbuild_properties_file} file not found")
|
||||
if use_local_sdk == False and os.environ.get("TACTILITY_SDK_PATH") is not None:
|
||||
print_warning("TACTILITY_SDK_PATH is set, but will be ignored by this command.")
|
||||
print_warning("If you want to use it, use the 'build local' parameters.")
|
||||
elif use_local_sdk == True and os.environ.get("TACTILITY_SDK_PATH") is None:
|
||||
exit_with_error("local build was requested, but TACTILITY_SDK_PATH environment variable is not set.")
|
||||
#endregion Core
|
||||
|
||||
def setup_environment():
|
||||
global ttbuild_path
|
||||
os.makedirs(ttbuild_path, exist_ok=True)
|
||||
#region SDK helpers
|
||||
|
||||
def read_sdk_json():
|
||||
json_file_path = os.path.join(ttbuild_path, "sdk.json")
|
||||
json_file = open(json_file_path)
|
||||
return json.load(json_file)
|
||||
|
||||
def get_sdk_dir(version, platform):
|
||||
global use_local_sdk
|
||||
@ -136,15 +130,6 @@ def get_sdk_dir(version, platform):
|
||||
global ttbuild_cdn
|
||||
return os.path.join(ttbuild_path, f"{version}-{platform}", "TactilitySDK")
|
||||
|
||||
def get_sdk_version():
|
||||
global ttbuild_properties_file
|
||||
parser = configparser.RawConfigParser()
|
||||
parser.read(ttbuild_properties_file)
|
||||
sdk_dict = dict(parser.items("sdk"))
|
||||
if not "version" in sdk_dict:
|
||||
exit_with_error(f"Could not find 'version' in [sdk] section in {ttbuild_properties_file}")
|
||||
return sdk_dict["version"]
|
||||
|
||||
def get_sdk_root_dir(version, platform):
|
||||
global ttbuild_cdn
|
||||
return os.path.join(ttbuild_path, f"{version}-{platform}")
|
||||
@ -175,20 +160,34 @@ def update_sdk_json():
|
||||
json_filepath = os.path.join(ttbuild_path, "sdk.json")
|
||||
return download_file(json_url, json_filepath)
|
||||
|
||||
def should_fetch_sdkconfig_files():
|
||||
def should_fetch_sdkconfig_files(platform_targets):
|
||||
for platform in platform_targets:
|
||||
sdkconfig_filename = f"sdkconfig.app.{platform}"
|
||||
if not os.path.exists(os.path.join(ttbuild_path, sdkconfig_filename)):
|
||||
return True
|
||||
return False
|
||||
|
||||
def fetch_sdkconfig_files():
|
||||
def fetch_sdkconfig_files(platform_targets):
|
||||
for platform in platform_targets:
|
||||
sdkconfig_filename = f"sdkconfig.app.{platform}"
|
||||
target_path = os.path.join(ttbuild_path, sdkconfig_filename)
|
||||
if not download_file(f"{ttbuild_cdn}/{sdkconfig_filename}", target_path):
|
||||
exit_with_error(f"Failed to download sdkconfig file for {platform}")
|
||||
|
||||
#endregion SDK helpers
|
||||
|
||||
#region Validation
|
||||
|
||||
def validate_environment():
|
||||
if os.environ.get("IDF_PATH") is None:
|
||||
exit_with_error("Cannot find the Espressif IDF SDK. Ensure it is installed and that it is activated via $PATH_TO_IDF_SDK/export.sh")
|
||||
if not os.path.exists("manifest.properties"):
|
||||
exit_with_error("manifest.properties not found")
|
||||
if use_local_sdk == False and os.environ.get("TACTILITY_SDK_PATH") is not None:
|
||||
print_warning("TACTILITY_SDK_PATH is set, but will be ignored by this command.")
|
||||
print_warning("If you want to use it, use the 'build local' parameters.")
|
||||
elif use_local_sdk == True and os.environ.get("TACTILITY_SDK_PATH") is None:
|
||||
exit_with_error("local build was requested, but TACTILITY_SDK_PATH environment variable is not set.")
|
||||
|
||||
def validate_version_and_platforms(sdk_json, sdk_version, platforms_to_build):
|
||||
version_map = sdk_json["versions"]
|
||||
@ -217,6 +216,64 @@ def validate_self(sdk_json):
|
||||
print_error("Run 'tactility.py updateself' to update.")
|
||||
sys.exit(1)
|
||||
|
||||
#endregion Validation
|
||||
|
||||
#region Manifest
|
||||
|
||||
def read_manifest():
|
||||
return read_properties_file("manifest.properties")
|
||||
|
||||
def validate_manifest(manifest):
|
||||
# [manifest]
|
||||
if not "manifest" in manifest:
|
||||
exit_with_error("Invalid manifest format: [manifest] not found")
|
||||
if not "version" in manifest["manifest"]:
|
||||
exit_with_error("Invalid manifest format: [manifest] version not found")
|
||||
# [target]
|
||||
if not "target" in manifest:
|
||||
exit_with_error("Invalid manifest format: [target] not found")
|
||||
if not "sdk" in manifest["target"]:
|
||||
exit_with_error("Invalid manifest format: [target] sdk not found")
|
||||
if not "platforms" in manifest["target"]:
|
||||
exit_with_error("Invalid manifest format: [target] platforms not found")
|
||||
# [app]
|
||||
if not "app" in manifest:
|
||||
exit_with_error("Invalid manifest format: [app] not found")
|
||||
if not "id" in manifest["app"]:
|
||||
exit_with_error("Invalid manifest format: [app] id not found")
|
||||
if not "version" in manifest["app"]:
|
||||
exit_with_error("Invalid manifest format: [app] version not found")
|
||||
if not "name" in manifest["app"]:
|
||||
exit_with_error("Invalid manifest format: [app] name not found")
|
||||
if not "description" in manifest["app"]:
|
||||
exit_with_error("Invalid manifest format: [app] description not found")
|
||||
# [author]
|
||||
if not "author" in manifest:
|
||||
exit_with_error("Invalid manifest format: [author] not found")
|
||||
if not "name" in manifest["author"]:
|
||||
exit_with_error("Invalid manifest format: [author] name not found")
|
||||
if not "website" in manifest["author"]:
|
||||
exit_with_error("Invalid manifest format: [author] website not found")
|
||||
|
||||
def is_valid_manifest_platform(manifest, platform):
|
||||
manifest_platforms = manifest["target"]["platforms"].split(",")
|
||||
return platform in manifest_platforms
|
||||
|
||||
def validate_manifest_platform(manifest, platform):
|
||||
if not is_valid_manifest_platform(manifest, platform):
|
||||
exit_with_error(f"Platform {platform} is not available in the manifest.")
|
||||
|
||||
def get_manifest_target_platforms(manifest, requested_platform):
|
||||
if requested_platform == "" or requested_platform is None:
|
||||
return manifest["target"]["platforms"].split(",")
|
||||
else:
|
||||
validate_manifest_platform(manifest, requested_platform)
|
||||
return [requested_platform]
|
||||
|
||||
#endregion Manifest
|
||||
|
||||
#region SDK download
|
||||
|
||||
def sdk_download(version, platform):
|
||||
sdk_root_dir = get_sdk_root_dir(version, platform)
|
||||
os.makedirs(sdk_root_dir, exist_ok=True)
|
||||
@ -240,12 +297,19 @@ def sdk_download_all(version, platforms):
|
||||
print(f"Using cached download for SDK version {version} and platform {platform}")
|
||||
return True
|
||||
|
||||
#endregion SDK download
|
||||
|
||||
#region Building
|
||||
|
||||
def get_cmake_path(platform):
|
||||
return os.path.join("build", f"cmake-build-{platform}")
|
||||
|
||||
def find_elf_file(platform):
|
||||
build_dir = f"build-{platform}"
|
||||
if os.path.exists(build_dir):
|
||||
for file in os.listdir(build_dir):
|
||||
cmake_dir = get_cmake_path(platform)
|
||||
if os.path.exists(cmake_dir):
|
||||
for file in os.listdir(cmake_dir):
|
||||
if file.endswith(".app.elf"):
|
||||
return os.path.join(build_dir, file)
|
||||
return os.path.join(cmake_dir, file)
|
||||
return None
|
||||
|
||||
def build_all(version, platforms, skip_build):
|
||||
@ -295,7 +359,8 @@ def build_first(version, platform, skip_build):
|
||||
if skip_build:
|
||||
return True
|
||||
print("Building first build")
|
||||
with subprocess.Popen(["idf.py", "-B", f"build-{platform}", "build"], stdout=subprocess.PIPE, stderr=subprocess.STDOUT) as process:
|
||||
cmake_path = get_cmake_path(platform)
|
||||
with subprocess.Popen(["idf.py", "-B", cmake_path, "build"], stdout=subprocess.PIPE, stderr=subprocess.STDOUT) as process:
|
||||
build_output = wait_for_build(process, platform)
|
||||
# The return code is never expected to be 0 due to a bug in the elf cmake script, but we keep it just in case
|
||||
if process.returncode == 0:
|
||||
@ -320,7 +385,8 @@ def build_consecutively(version, platform, skip_build):
|
||||
os.system(f"cp {sdkconfig_path} sdkconfig")
|
||||
if skip_build:
|
||||
return True
|
||||
with subprocess.Popen(["idf.py", "-B", f"build-{platform}", "elf"], stdout=subprocess.PIPE, stderr=subprocess.STDOUT) as process:
|
||||
cmake_path = get_cmake_path(platform)
|
||||
with subprocess.Popen(["idf.py", "-B", cmake_path, "elf"], stdout=subprocess.PIPE, stderr=subprocess.STDOUT) as process:
|
||||
build_output = wait_for_build(process, platform)
|
||||
if process.returncode == 0:
|
||||
print(f"{shell_color_green}Building for {platform} ✅{shell_color_reset}")
|
||||
@ -331,41 +397,85 @@ def build_consecutively(version, platform, skip_build):
|
||||
print(f"{shell_color_red}Building for {platform} failed ❌{shell_color_reset}")
|
||||
return False
|
||||
|
||||
def read_sdk_json():
|
||||
json_file_path = os.path.join(ttbuild_path, "sdk.json")
|
||||
json_file = open(json_file_path)
|
||||
return json.load(json_file)
|
||||
#endregion Building
|
||||
|
||||
def build_action(platform_arg):
|
||||
#region Packaging
|
||||
|
||||
def package_intermediate_manifest(target_path):
|
||||
if not os.path.isfile("manifest.properties"):
|
||||
print_error("manifest.properties not found")
|
||||
return
|
||||
shutil.copy("manifest.properties", os.path.join(target_path, "manifest.properties"))
|
||||
|
||||
def package_intermediate_binaries(target_path, platforms):
|
||||
elf_dir = os.path.join(target_path, "elf")
|
||||
os.makedirs(elf_dir, exist_ok=True)
|
||||
for platform in platforms:
|
||||
elf_path = find_elf_file(platform)
|
||||
if elf_path is None:
|
||||
print_error(f"ELF file not found at {elf_path}")
|
||||
return
|
||||
shutil.copy(elf_path, os.path.join(elf_dir, f"{platform}.elf"))
|
||||
|
||||
def package_intermediate_assets(target_path):
|
||||
if os.path.isdir("assets"):
|
||||
shutil.copytree("assets", os.path.join(target_path, "assets"), dirs_exist_ok=True)
|
||||
|
||||
def package_intermediate(platforms):
|
||||
target_path = os.path.join("build", "package-intermediate")
|
||||
if os.path.isdir(target_path):
|
||||
shutil.rmtree(target_path)
|
||||
os.makedirs(target_path, exist_ok=True)
|
||||
package_intermediate_manifest(target_path)
|
||||
package_intermediate_binaries(target_path, platforms)
|
||||
package_intermediate_assets(target_path)
|
||||
|
||||
def package_name(platforms):
|
||||
elf_path = find_elf_file(platforms[0])
|
||||
elf_base_name = os.path.basename(elf_path).removesuffix(".app.elf")
|
||||
return os.path.join("build", f"{elf_base_name}.app")
|
||||
|
||||
def package_all(platforms):
|
||||
print("Packaging app")
|
||||
package_intermediate(platforms)
|
||||
# Create build/something.app
|
||||
tar_path = package_name(platforms)
|
||||
tar = tarfile.open(tar_path, mode="w", format=tarfile.USTAR_FORMAT)
|
||||
tar.add(os.path.join("build", "package-intermediate"), arcname="")
|
||||
tar.close()
|
||||
|
||||
#endregion Packaging
|
||||
|
||||
def setup_environment():
|
||||
global ttbuild_path
|
||||
os.makedirs(ttbuild_path, exist_ok=True)
|
||||
|
||||
def build_action(manifest, platform_arg):
|
||||
# Environment validation
|
||||
validate_environment()
|
||||
platforms_to_build = platform_targets if platform_arg == "all" else [platform_arg]
|
||||
if not is_valid_platform_name(platform_arg):
|
||||
print_help()
|
||||
exit_with_error("Invalid platform name")
|
||||
platforms_to_build = get_manifest_target_platforms(manifest, platform_arg)
|
||||
if not use_local_sdk:
|
||||
if should_fetch_sdkconfig_files():
|
||||
fetch_sdkconfig_files()
|
||||
if should_fetch_sdkconfig_files(platforms_to_build):
|
||||
fetch_sdkconfig_files(platforms_to_build)
|
||||
sdk_json = read_sdk_json()
|
||||
validate_self(sdk_json)
|
||||
if not "versions" in sdk_json:
|
||||
exit_with_error("Version data not found in sdk.json")
|
||||
# Build
|
||||
sdk_version = get_sdk_version()
|
||||
sdk_version = manifest["target"]["sdk"]
|
||||
if not use_local_sdk:
|
||||
validate_version_and_platforms(sdk_json, sdk_version, platforms_to_build)
|
||||
if not sdk_download_all(sdk_version, platforms_to_build):
|
||||
exit_with_error("Failed to download one or more SDKs")
|
||||
build_all(sdk_version, platforms_to_build, skip_build) # Environment validation
|
||||
if not skip_build:
|
||||
package_all(platforms_to_build)
|
||||
|
||||
def clean_action():
|
||||
count = 0
|
||||
for path in os.listdir("."):
|
||||
if path.startswith("build-"):
|
||||
print(f"Removing {path}/")
|
||||
shutil.rmtree(path)
|
||||
count = count + 1
|
||||
if count == 0:
|
||||
if os.path.exists("build"):
|
||||
print(f"Removing build/")
|
||||
shutil.rmtree("build")
|
||||
else:
|
||||
print("Nothing to clean")
|
||||
|
||||
def clear_cache_action():
|
||||
@ -396,7 +506,8 @@ def get_device_info(ip):
|
||||
except requests.RequestException as e:
|
||||
print(f"Request failed: {e}")
|
||||
|
||||
def run_action(ip, app_id):
|
||||
def run_action(manifest, ip):
|
||||
app_id = manifest["app"]["id"]
|
||||
print(f"Running {app_id} on {ip}")
|
||||
url = get_url(ip, "/app/run")
|
||||
params = {'id': app_id}
|
||||
@ -409,16 +520,17 @@ def run_action(ip, app_id):
|
||||
except requests.RequestException as e:
|
||||
print(f"Request failed: {e}")
|
||||
|
||||
def install_action(ip, platform):
|
||||
file_path = find_elf_file(platform)
|
||||
if file_path is None:
|
||||
print_error(f"File not found: {file_path}")
|
||||
return
|
||||
print(f"Installing {file_path} to {ip}")
|
||||
def install_action(ip, platforms):
|
||||
for platform in platforms:
|
||||
elf_path = find_elf_file(platform)
|
||||
if elf_path is None:
|
||||
exit_with_error(f"ELF file not built for {platform}")
|
||||
package_path = package_name(platforms)
|
||||
print(f"Installing {package_path} to {ip}")
|
||||
url = get_url(ip, "/app/install")
|
||||
try:
|
||||
# Prepare multipart form data
|
||||
with open(file_path, 'rb') as file:
|
||||
with open(package_path, 'rb') as file:
|
||||
files = {
|
||||
'elf': file
|
||||
}
|
||||
@ -432,6 +544,7 @@ def install_action(ip, platform):
|
||||
except IOError as e:
|
||||
print_error(f"File error: {e}")
|
||||
|
||||
#region Main
|
||||
|
||||
if __name__ == "__main__":
|
||||
print(f"Tactility Build System v{ttbuild_version}")
|
||||
@ -448,15 +561,23 @@ if __name__ == "__main__":
|
||||
use_local_sdk = "--local-sdk" in sys.argv
|
||||
# Environment setup
|
||||
setup_environment()
|
||||
if not os.path.isfile("manifest.properties"):
|
||||
exit_with_error("manifest.properties not found")
|
||||
manifest = read_manifest()
|
||||
validate_manifest(manifest)
|
||||
all_platform_targets = manifest["target"]["platforms"].split(",")
|
||||
# Update SDK cache (sdk.json)
|
||||
if should_update_sdk_json() and not update_sdk_json():
|
||||
exit_with_error("Failed to retrieve SDK info")
|
||||
# Actions
|
||||
if action_arg == "build":
|
||||
if len(sys.argv) < 3:
|
||||
if len(sys.argv) < 2:
|
||||
print_help()
|
||||
exit_with_error("Commandline parameter missing")
|
||||
build_action(sys.argv[2])
|
||||
platform = None
|
||||
if len(sys.argv) > 2:
|
||||
platform = sys.argv[2]
|
||||
build_action(manifest, platform)
|
||||
elif action_arg == "clean":
|
||||
clean_action()
|
||||
elif action_arg == "clearcache":
|
||||
@ -464,15 +585,34 @@ if __name__ == "__main__":
|
||||
elif action_arg == "updateself":
|
||||
update_self_action()
|
||||
elif action_arg == "run":
|
||||
if len(sys.argv) < 4:
|
||||
if len(sys.argv) < 3:
|
||||
print_help()
|
||||
exit_with_error("Commandline parameter missing")
|
||||
run_action(sys.argv[2], sys.argv[3])
|
||||
run_action(manifest, sys.argv[2])
|
||||
elif action_arg == "install":
|
||||
if len(sys.argv) < 4:
|
||||
if len(sys.argv) < 3:
|
||||
print_help()
|
||||
exit_with_error("Commandline parameter missing")
|
||||
install_action(sys.argv[2], sys.argv[3])
|
||||
platform = None
|
||||
platforms_to_install = all_platform_targets
|
||||
if len(sys.argv) >= 4:
|
||||
platform = sys.argv[3]
|
||||
platforms_to_install = [platform]
|
||||
install_action(sys.argv[2], platforms_to_install)
|
||||
elif action_arg == "bir":
|
||||
if len(sys.argv) < 3:
|
||||
print_help()
|
||||
exit_with_error("Commandline parameter missing")
|
||||
platform = None
|
||||
platforms_to_install = all_platform_targets
|
||||
if len(sys.argv) >= 4:
|
||||
platform = sys.argv[3]
|
||||
platforms_to_install = [platform]
|
||||
build_action(manifest, platform)
|
||||
install_action(sys.argv[2], platforms_to_install)
|
||||
run_action(manifest, sys.argv[2])
|
||||
else:
|
||||
print_help()
|
||||
exit_with_error("Unknown commandline parameter")
|
||||
|
||||
#endregion Main
|
||||
|
||||
13
ExternalApps/GraphicsDemo/manifest.properties
Normal file
13
ExternalApps/GraphicsDemo/manifest.properties
Normal file
@ -0,0 +1,13 @@
|
||||
[manifest]
|
||||
version=0.1
|
||||
[target]
|
||||
sdk=0.5.0
|
||||
platforms=esp32,esp32s3
|
||||
[app]
|
||||
id=com.bytewelder.graphicsdemo
|
||||
version=0.1.0
|
||||
name=Graphics Demo
|
||||
description=A graphics and touch driver demonstration
|
||||
[author]
|
||||
name=ByteWelder
|
||||
website=https://bytewelder.com
|
||||
@ -1,2 +0,0 @@
|
||||
[sdk]
|
||||
version = 0.5.0
|
||||
@ -8,22 +8,19 @@ import subprocess
|
||||
import time
|
||||
import urllib.request
|
||||
import zipfile
|
||||
|
||||
import requests
|
||||
import tarfile
|
||||
import shutil
|
||||
import configparser
|
||||
|
||||
# Targetable platforms that represent a specific hardware target
|
||||
platform_targets = ["esp32", "esp32s3"]
|
||||
# All valid platform commandline arguments
|
||||
platform_arguments = platform_targets.copy()
|
||||
platform_arguments.append("all")
|
||||
ttbuild_path = ".tactility"
|
||||
ttbuild_version = "1.2.1"
|
||||
ttbuild_properties_file = "tactility.properties"
|
||||
ttbuild_version = "2.0.0"
|
||||
ttbuild_cdn = "https://cdn.tactility.one"
|
||||
ttbuild_sdk_json_validity = 3600 # seconds
|
||||
ttport = 6666
|
||||
verbose = False
|
||||
use_local_sdk = False
|
||||
valid_platforms = ["esp32", "esp32s3"]
|
||||
|
||||
spinner_pattern = [
|
||||
"⠋",
|
||||
@ -57,21 +54,24 @@ def print_help():
|
||||
print("Usage: python tactility.py [action] [options]")
|
||||
print("")
|
||||
print("Actions:")
|
||||
print(" build [esp32,esp32s3,all,local] Build the app for the specified platform")
|
||||
print(" build [esp32,esp32s3] Build the app. Optionally specify a platform.")
|
||||
print(" esp32: ESP32")
|
||||
print(" esp32s3: ESP32 S3")
|
||||
print(" all: all supported ESP platforms")
|
||||
print(" clean Clean the build folders")
|
||||
print(" clearcache Clear the SDK cache")
|
||||
print(" updateself Update this tool")
|
||||
print(" run [ip] [app id] Run an application")
|
||||
print(" install [ip] [esp32,esp32s3] Install an application")
|
||||
print(" clean Clean the build folders")
|
||||
print(" clearcache Clear the SDK cache")
|
||||
print(" updateself Update this tool")
|
||||
print(" run [ip] Run an application")
|
||||
print(" install [ip] Install an application")
|
||||
print(" bir [ip] [esp32,esp32s3] Build, install then run. Optionally specify a platform.")
|
||||
print(" brrr [ip] [esp32,esp32s3] Functionally the same as \"bir\", but \"app goes brrr\" meme variant.")
|
||||
print("")
|
||||
print("Options:")
|
||||
print(" --help Show this commandline info")
|
||||
print(" --local-sdk Use SDK specified by environment variable TACTILITY_SDK_PATH")
|
||||
print(" --skip-build Run everything except the idf.py/CMake commands")
|
||||
print(" --verbose Show extra console output")
|
||||
print(" --help Show this commandline info")
|
||||
print(" --local-sdk Use SDK specified by environment variable TACTILITY_SDK_PATH")
|
||||
print(" --skip-build Run everything except the idf.py/CMake commands")
|
||||
print(" --verbose Show extra console output")
|
||||
|
||||
# region Core
|
||||
|
||||
def download_file(url, filepath):
|
||||
global verbose
|
||||
@ -108,25 +108,19 @@ def exit_with_error(message):
|
||||
def get_url(ip, path):
|
||||
return f"http://{ip}:{ttport}{path}"
|
||||
|
||||
def is_valid_platform_name(name):
|
||||
global platform_arguments
|
||||
return name in platform_arguments
|
||||
def read_properties_file(path):
|
||||
config = configparser.RawConfigParser()
|
||||
config.read(path)
|
||||
return config
|
||||
|
||||
def validate_environment():
|
||||
global ttbuild_properties_file, use_local_sdk
|
||||
if os.environ.get("IDF_PATH") is None:
|
||||
exit_with_error("Cannot find the Espressif IDF SDK. Ensure it is installed and that it is activated via $PATH_TO_IDF_SDK/export.sh")
|
||||
if not os.path.exists(ttbuild_properties_file):
|
||||
exit_with_error(f"{ttbuild_properties_file} file not found")
|
||||
if use_local_sdk == False and os.environ.get("TACTILITY_SDK_PATH") is not None:
|
||||
print_warning("TACTILITY_SDK_PATH is set, but will be ignored by this command.")
|
||||
print_warning("If you want to use it, use the 'build local' parameters.")
|
||||
elif use_local_sdk == True and os.environ.get("TACTILITY_SDK_PATH") is None:
|
||||
exit_with_error("local build was requested, but TACTILITY_SDK_PATH environment variable is not set.")
|
||||
#endregion Core
|
||||
|
||||
def setup_environment():
|
||||
global ttbuild_path
|
||||
os.makedirs(ttbuild_path, exist_ok=True)
|
||||
#region SDK helpers
|
||||
|
||||
def read_sdk_json():
|
||||
json_file_path = os.path.join(ttbuild_path, "sdk.json")
|
||||
json_file = open(json_file_path)
|
||||
return json.load(json_file)
|
||||
|
||||
def get_sdk_dir(version, platform):
|
||||
global use_local_sdk
|
||||
@ -136,15 +130,6 @@ def get_sdk_dir(version, platform):
|
||||
global ttbuild_cdn
|
||||
return os.path.join(ttbuild_path, f"{version}-{platform}", "TactilitySDK")
|
||||
|
||||
def get_sdk_version():
|
||||
global ttbuild_properties_file
|
||||
parser = configparser.RawConfigParser()
|
||||
parser.read(ttbuild_properties_file)
|
||||
sdk_dict = dict(parser.items("sdk"))
|
||||
if not "version" in sdk_dict:
|
||||
exit_with_error(f"Could not find 'version' in [sdk] section in {ttbuild_properties_file}")
|
||||
return sdk_dict["version"]
|
||||
|
||||
def get_sdk_root_dir(version, platform):
|
||||
global ttbuild_cdn
|
||||
return os.path.join(ttbuild_path, f"{version}-{platform}")
|
||||
@ -175,20 +160,34 @@ def update_sdk_json():
|
||||
json_filepath = os.path.join(ttbuild_path, "sdk.json")
|
||||
return download_file(json_url, json_filepath)
|
||||
|
||||
def should_fetch_sdkconfig_files():
|
||||
def should_fetch_sdkconfig_files(platform_targets):
|
||||
for platform in platform_targets:
|
||||
sdkconfig_filename = f"sdkconfig.app.{platform}"
|
||||
if not os.path.exists(os.path.join(ttbuild_path, sdkconfig_filename)):
|
||||
return True
|
||||
return False
|
||||
|
||||
def fetch_sdkconfig_files():
|
||||
def fetch_sdkconfig_files(platform_targets):
|
||||
for platform in platform_targets:
|
||||
sdkconfig_filename = f"sdkconfig.app.{platform}"
|
||||
target_path = os.path.join(ttbuild_path, sdkconfig_filename)
|
||||
if not download_file(f"{ttbuild_cdn}/{sdkconfig_filename}", target_path):
|
||||
exit_with_error(f"Failed to download sdkconfig file for {platform}")
|
||||
|
||||
#endregion SDK helpers
|
||||
|
||||
#region Validation
|
||||
|
||||
def validate_environment():
|
||||
if os.environ.get("IDF_PATH") is None:
|
||||
exit_with_error("Cannot find the Espressif IDF SDK. Ensure it is installed and that it is activated via $PATH_TO_IDF_SDK/export.sh")
|
||||
if not os.path.exists("manifest.properties"):
|
||||
exit_with_error("manifest.properties not found")
|
||||
if use_local_sdk == False and os.environ.get("TACTILITY_SDK_PATH") is not None:
|
||||
print_warning("TACTILITY_SDK_PATH is set, but will be ignored by this command.")
|
||||
print_warning("If you want to use it, use the 'build local' parameters.")
|
||||
elif use_local_sdk == True and os.environ.get("TACTILITY_SDK_PATH") is None:
|
||||
exit_with_error("local build was requested, but TACTILITY_SDK_PATH environment variable is not set.")
|
||||
|
||||
def validate_version_and_platforms(sdk_json, sdk_version, platforms_to_build):
|
||||
version_map = sdk_json["versions"]
|
||||
@ -217,6 +216,64 @@ def validate_self(sdk_json):
|
||||
print_error("Run 'tactility.py updateself' to update.")
|
||||
sys.exit(1)
|
||||
|
||||
#endregion Validation
|
||||
|
||||
#region Manifest
|
||||
|
||||
def read_manifest():
|
||||
return read_properties_file("manifest.properties")
|
||||
|
||||
def validate_manifest(manifest):
|
||||
# [manifest]
|
||||
if not "manifest" in manifest:
|
||||
exit_with_error("Invalid manifest format: [manifest] not found")
|
||||
if not "version" in manifest["manifest"]:
|
||||
exit_with_error("Invalid manifest format: [manifest] version not found")
|
||||
# [target]
|
||||
if not "target" in manifest:
|
||||
exit_with_error("Invalid manifest format: [target] not found")
|
||||
if not "sdk" in manifest["target"]:
|
||||
exit_with_error("Invalid manifest format: [target] sdk not found")
|
||||
if not "platforms" in manifest["target"]:
|
||||
exit_with_error("Invalid manifest format: [target] platforms not found")
|
||||
# [app]
|
||||
if not "app" in manifest:
|
||||
exit_with_error("Invalid manifest format: [app] not found")
|
||||
if not "id" in manifest["app"]:
|
||||
exit_with_error("Invalid manifest format: [app] id not found")
|
||||
if not "version" in manifest["app"]:
|
||||
exit_with_error("Invalid manifest format: [app] version not found")
|
||||
if not "name" in manifest["app"]:
|
||||
exit_with_error("Invalid manifest format: [app] name not found")
|
||||
if not "description" in manifest["app"]:
|
||||
exit_with_error("Invalid manifest format: [app] description not found")
|
||||
# [author]
|
||||
if not "author" in manifest:
|
||||
exit_with_error("Invalid manifest format: [author] not found")
|
||||
if not "name" in manifest["author"]:
|
||||
exit_with_error("Invalid manifest format: [author] name not found")
|
||||
if not "website" in manifest["author"]:
|
||||
exit_with_error("Invalid manifest format: [author] website not found")
|
||||
|
||||
def is_valid_manifest_platform(manifest, platform):
|
||||
manifest_platforms = manifest["target"]["platforms"].split(",")
|
||||
return platform in manifest_platforms
|
||||
|
||||
def validate_manifest_platform(manifest, platform):
|
||||
if not is_valid_manifest_platform(manifest, platform):
|
||||
exit_with_error(f"Platform {platform} is not available in the manifest.")
|
||||
|
||||
def get_manifest_target_platforms(manifest, requested_platform):
|
||||
if requested_platform == "" or requested_platform is None:
|
||||
return manifest["target"]["platforms"].split(",")
|
||||
else:
|
||||
validate_manifest_platform(manifest, requested_platform)
|
||||
return [requested_platform]
|
||||
|
||||
#endregion Manifest
|
||||
|
||||
#region SDK download
|
||||
|
||||
def sdk_download(version, platform):
|
||||
sdk_root_dir = get_sdk_root_dir(version, platform)
|
||||
os.makedirs(sdk_root_dir, exist_ok=True)
|
||||
@ -240,12 +297,19 @@ def sdk_download_all(version, platforms):
|
||||
print(f"Using cached download for SDK version {version} and platform {platform}")
|
||||
return True
|
||||
|
||||
#endregion SDK download
|
||||
|
||||
#region Building
|
||||
|
||||
def get_cmake_path(platform):
|
||||
return os.path.join("build", f"cmake-build-{platform}")
|
||||
|
||||
def find_elf_file(platform):
|
||||
build_dir = f"build-{platform}"
|
||||
if os.path.exists(build_dir):
|
||||
for file in os.listdir(build_dir):
|
||||
cmake_dir = get_cmake_path(platform)
|
||||
if os.path.exists(cmake_dir):
|
||||
for file in os.listdir(cmake_dir):
|
||||
if file.endswith(".app.elf"):
|
||||
return os.path.join(build_dir, file)
|
||||
return os.path.join(cmake_dir, file)
|
||||
return None
|
||||
|
||||
def build_all(version, platforms, skip_build):
|
||||
@ -295,7 +359,8 @@ def build_first(version, platform, skip_build):
|
||||
if skip_build:
|
||||
return True
|
||||
print("Building first build")
|
||||
with subprocess.Popen(["idf.py", "-B", f"build-{platform}", "build"], stdout=subprocess.PIPE, stderr=subprocess.STDOUT) as process:
|
||||
cmake_path = get_cmake_path(platform)
|
||||
with subprocess.Popen(["idf.py", "-B", cmake_path, "build"], stdout=subprocess.PIPE, stderr=subprocess.STDOUT) as process:
|
||||
build_output = wait_for_build(process, platform)
|
||||
# The return code is never expected to be 0 due to a bug in the elf cmake script, but we keep it just in case
|
||||
if process.returncode == 0:
|
||||
@ -320,7 +385,8 @@ def build_consecutively(version, platform, skip_build):
|
||||
os.system(f"cp {sdkconfig_path} sdkconfig")
|
||||
if skip_build:
|
||||
return True
|
||||
with subprocess.Popen(["idf.py", "-B", f"build-{platform}", "elf"], stdout=subprocess.PIPE, stderr=subprocess.STDOUT) as process:
|
||||
cmake_path = get_cmake_path(platform)
|
||||
with subprocess.Popen(["idf.py", "-B", cmake_path, "elf"], stdout=subprocess.PIPE, stderr=subprocess.STDOUT) as process:
|
||||
build_output = wait_for_build(process, platform)
|
||||
if process.returncode == 0:
|
||||
print(f"{shell_color_green}Building for {platform} ✅{shell_color_reset}")
|
||||
@ -331,41 +397,85 @@ def build_consecutively(version, platform, skip_build):
|
||||
print(f"{shell_color_red}Building for {platform} failed ❌{shell_color_reset}")
|
||||
return False
|
||||
|
||||
def read_sdk_json():
|
||||
json_file_path = os.path.join(ttbuild_path, "sdk.json")
|
||||
json_file = open(json_file_path)
|
||||
return json.load(json_file)
|
||||
#endregion Building
|
||||
|
||||
def build_action(platform_arg):
|
||||
#region Packaging
|
||||
|
||||
def package_intermediate_manifest(target_path):
|
||||
if not os.path.isfile("manifest.properties"):
|
||||
print_error("manifest.properties not found")
|
||||
return
|
||||
shutil.copy("manifest.properties", os.path.join(target_path, "manifest.properties"))
|
||||
|
||||
def package_intermediate_binaries(target_path, platforms):
|
||||
elf_dir = os.path.join(target_path, "elf")
|
||||
os.makedirs(elf_dir, exist_ok=True)
|
||||
for platform in platforms:
|
||||
elf_path = find_elf_file(platform)
|
||||
if elf_path is None:
|
||||
print_error(f"ELF file not found at {elf_path}")
|
||||
return
|
||||
shutil.copy(elf_path, os.path.join(elf_dir, f"{platform}.elf"))
|
||||
|
||||
def package_intermediate_assets(target_path):
|
||||
if os.path.isdir("assets"):
|
||||
shutil.copytree("assets", os.path.join(target_path, "assets"), dirs_exist_ok=True)
|
||||
|
||||
def package_intermediate(platforms):
|
||||
target_path = os.path.join("build", "package-intermediate")
|
||||
if os.path.isdir(target_path):
|
||||
shutil.rmtree(target_path)
|
||||
os.makedirs(target_path, exist_ok=True)
|
||||
package_intermediate_manifest(target_path)
|
||||
package_intermediate_binaries(target_path, platforms)
|
||||
package_intermediate_assets(target_path)
|
||||
|
||||
def package_name(platforms):
|
||||
elf_path = find_elf_file(platforms[0])
|
||||
elf_base_name = os.path.basename(elf_path).removesuffix(".app.elf")
|
||||
return os.path.join("build", f"{elf_base_name}.app")
|
||||
|
||||
def package_all(platforms):
|
||||
print("Packaging app")
|
||||
package_intermediate(platforms)
|
||||
# Create build/something.app
|
||||
tar_path = package_name(platforms)
|
||||
tar = tarfile.open(tar_path, mode="w", format=tarfile.USTAR_FORMAT)
|
||||
tar.add(os.path.join("build", "package-intermediate"), arcname="")
|
||||
tar.close()
|
||||
|
||||
#endregion Packaging
|
||||
|
||||
def setup_environment():
|
||||
global ttbuild_path
|
||||
os.makedirs(ttbuild_path, exist_ok=True)
|
||||
|
||||
def build_action(manifest, platform_arg):
|
||||
# Environment validation
|
||||
validate_environment()
|
||||
platforms_to_build = platform_targets if platform_arg == "all" else [platform_arg]
|
||||
if not is_valid_platform_name(platform_arg):
|
||||
print_help()
|
||||
exit_with_error("Invalid platform name")
|
||||
platforms_to_build = get_manifest_target_platforms(manifest, platform_arg)
|
||||
if not use_local_sdk:
|
||||
if should_fetch_sdkconfig_files():
|
||||
fetch_sdkconfig_files()
|
||||
if should_fetch_sdkconfig_files(platforms_to_build):
|
||||
fetch_sdkconfig_files(platforms_to_build)
|
||||
sdk_json = read_sdk_json()
|
||||
validate_self(sdk_json)
|
||||
if not "versions" in sdk_json:
|
||||
exit_with_error("Version data not found in sdk.json")
|
||||
# Build
|
||||
sdk_version = get_sdk_version()
|
||||
sdk_version = manifest["target"]["sdk"]
|
||||
if not use_local_sdk:
|
||||
validate_version_and_platforms(sdk_json, sdk_version, platforms_to_build)
|
||||
if not sdk_download_all(sdk_version, platforms_to_build):
|
||||
exit_with_error("Failed to download one or more SDKs")
|
||||
build_all(sdk_version, platforms_to_build, skip_build) # Environment validation
|
||||
if not skip_build:
|
||||
package_all(platforms_to_build)
|
||||
|
||||
def clean_action():
|
||||
count = 0
|
||||
for path in os.listdir("."):
|
||||
if path.startswith("build-"):
|
||||
print(f"Removing {path}/")
|
||||
shutil.rmtree(path)
|
||||
count = count + 1
|
||||
if count == 0:
|
||||
if os.path.exists("build"):
|
||||
print(f"Removing build/")
|
||||
shutil.rmtree("build")
|
||||
else:
|
||||
print("Nothing to clean")
|
||||
|
||||
def clear_cache_action():
|
||||
@ -396,7 +506,8 @@ def get_device_info(ip):
|
||||
except requests.RequestException as e:
|
||||
print(f"Request failed: {e}")
|
||||
|
||||
def run_action(ip, app_id):
|
||||
def run_action(manifest, ip):
|
||||
app_id = manifest["app"]["id"]
|
||||
print(f"Running {app_id} on {ip}")
|
||||
url = get_url(ip, "/app/run")
|
||||
params = {'id': app_id}
|
||||
@ -409,16 +520,17 @@ def run_action(ip, app_id):
|
||||
except requests.RequestException as e:
|
||||
print(f"Request failed: {e}")
|
||||
|
||||
def install_action(ip, platform):
|
||||
file_path = find_elf_file(platform)
|
||||
if file_path is None:
|
||||
print_error(f"File not found: {file_path}")
|
||||
return
|
||||
print(f"Installing {file_path} to {ip}")
|
||||
def install_action(ip, platforms):
|
||||
for platform in platforms:
|
||||
elf_path = find_elf_file(platform)
|
||||
if elf_path is None:
|
||||
exit_with_error(f"ELF file not built for {platform}")
|
||||
package_path = package_name(platforms)
|
||||
print(f"Installing {package_path} to {ip}")
|
||||
url = get_url(ip, "/app/install")
|
||||
try:
|
||||
# Prepare multipart form data
|
||||
with open(file_path, 'rb') as file:
|
||||
with open(package_path, 'rb') as file:
|
||||
files = {
|
||||
'elf': file
|
||||
}
|
||||
@ -432,6 +544,7 @@ def install_action(ip, platform):
|
||||
except IOError as e:
|
||||
print_error(f"File error: {e}")
|
||||
|
||||
#region Main
|
||||
|
||||
if __name__ == "__main__":
|
||||
print(f"Tactility Build System v{ttbuild_version}")
|
||||
@ -448,15 +561,23 @@ if __name__ == "__main__":
|
||||
use_local_sdk = "--local-sdk" in sys.argv
|
||||
# Environment setup
|
||||
setup_environment()
|
||||
if not os.path.isfile("manifest.properties"):
|
||||
exit_with_error("manifest.properties not found")
|
||||
manifest = read_manifest()
|
||||
validate_manifest(manifest)
|
||||
all_platform_targets = manifest["target"]["platforms"].split(",")
|
||||
# Update SDK cache (sdk.json)
|
||||
if should_update_sdk_json() and not update_sdk_json():
|
||||
exit_with_error("Failed to retrieve SDK info")
|
||||
# Actions
|
||||
if action_arg == "build":
|
||||
if len(sys.argv) < 3:
|
||||
if len(sys.argv) < 2:
|
||||
print_help()
|
||||
exit_with_error("Commandline parameter missing")
|
||||
build_action(sys.argv[2])
|
||||
platform = None
|
||||
if len(sys.argv) > 2:
|
||||
platform = sys.argv[2]
|
||||
build_action(manifest, platform)
|
||||
elif action_arg == "clean":
|
||||
clean_action()
|
||||
elif action_arg == "clearcache":
|
||||
@ -464,15 +585,34 @@ if __name__ == "__main__":
|
||||
elif action_arg == "updateself":
|
||||
update_self_action()
|
||||
elif action_arg == "run":
|
||||
if len(sys.argv) < 4:
|
||||
if len(sys.argv) < 3:
|
||||
print_help()
|
||||
exit_with_error("Commandline parameter missing")
|
||||
run_action(sys.argv[2], sys.argv[3])
|
||||
run_action(manifest, sys.argv[2])
|
||||
elif action_arg == "install":
|
||||
if len(sys.argv) < 4:
|
||||
if len(sys.argv) < 3:
|
||||
print_help()
|
||||
exit_with_error("Commandline parameter missing")
|
||||
install_action(sys.argv[2], sys.argv[3])
|
||||
platform = None
|
||||
platforms_to_install = all_platform_targets
|
||||
if len(sys.argv) >= 4:
|
||||
platform = sys.argv[3]
|
||||
platforms_to_install = [platform]
|
||||
install_action(sys.argv[2], platforms_to_install)
|
||||
elif action_arg == "bir":
|
||||
if len(sys.argv) < 3:
|
||||
print_help()
|
||||
exit_with_error("Commandline parameter missing")
|
||||
platform = None
|
||||
platforms_to_install = all_platform_targets
|
||||
if len(sys.argv) >= 4:
|
||||
platform = sys.argv[3]
|
||||
platforms_to_install = [platform]
|
||||
build_action(manifest, platform)
|
||||
install_action(sys.argv[2], platforms_to_install)
|
||||
run_action(manifest, sys.argv[2])
|
||||
else:
|
||||
print_help()
|
||||
exit_with_error("Unknown commandline parameter")
|
||||
|
||||
#endregion Main
|
||||
|
||||
2
ExternalApps/HelloWorld/.gitignore
vendored
2
ExternalApps/HelloWorld/.gitignore
vendored
@ -1,2 +1,2 @@
|
||||
build*/
|
||||
build/
|
||||
.tactility/
|
||||
|
||||
1
ExternalApps/HelloWorld/assets/message.txt
Normal file
1
ExternalApps/HelloWorld/assets/message.txt
Normal file
@ -0,0 +1 @@
|
||||
Hello, world!
|
||||
13
ExternalApps/HelloWorld/manifest.properties
Normal file
13
ExternalApps/HelloWorld/manifest.properties
Normal file
@ -0,0 +1,13 @@
|
||||
[manifest]
|
||||
version=0.1
|
||||
[target]
|
||||
sdk=0.5.0
|
||||
platforms=esp32,esp32s3
|
||||
[app]
|
||||
id=com.bytewelder.helloworld
|
||||
version=0.1.0
|
||||
name=Hello World
|
||||
description=A demonstration app that says hi
|
||||
[author]
|
||||
name=ByteWelder
|
||||
website=https://bytewelder.com
|
||||
@ -1,2 +0,0 @@
|
||||
[sdk]
|
||||
version = 0.5.0
|
||||
@ -8,22 +8,19 @@ import subprocess
|
||||
import time
|
||||
import urllib.request
|
||||
import zipfile
|
||||
|
||||
import requests
|
||||
import tarfile
|
||||
import shutil
|
||||
import configparser
|
||||
|
||||
# Targetable platforms that represent a specific hardware target
|
||||
platform_targets = ["esp32", "esp32s3"]
|
||||
# All valid platform commandline arguments
|
||||
platform_arguments = platform_targets.copy()
|
||||
platform_arguments.append("all")
|
||||
ttbuild_path = ".tactility"
|
||||
ttbuild_version = "1.2.1"
|
||||
ttbuild_properties_file = "tactility.properties"
|
||||
ttbuild_version = "2.0.0"
|
||||
ttbuild_cdn = "https://cdn.tactility.one"
|
||||
ttbuild_sdk_json_validity = 3600 # seconds
|
||||
ttport = 6666
|
||||
verbose = False
|
||||
use_local_sdk = False
|
||||
valid_platforms = ["esp32", "esp32s3"]
|
||||
|
||||
spinner_pattern = [
|
||||
"⠋",
|
||||
@ -57,21 +54,24 @@ def print_help():
|
||||
print("Usage: python tactility.py [action] [options]")
|
||||
print("")
|
||||
print("Actions:")
|
||||
print(" build [esp32,esp32s3,all,local] Build the app for the specified platform")
|
||||
print(" build [esp32,esp32s3] Build the app. Optionally specify a platform.")
|
||||
print(" esp32: ESP32")
|
||||
print(" esp32s3: ESP32 S3")
|
||||
print(" all: all supported ESP platforms")
|
||||
print(" clean Clean the build folders")
|
||||
print(" clearcache Clear the SDK cache")
|
||||
print(" updateself Update this tool")
|
||||
print(" run [ip] [app id] Run an application")
|
||||
print(" install [ip] [esp32,esp32s3] Install an application")
|
||||
print(" clean Clean the build folders")
|
||||
print(" clearcache Clear the SDK cache")
|
||||
print(" updateself Update this tool")
|
||||
print(" run [ip] Run an application")
|
||||
print(" install [ip] Install an application")
|
||||
print(" bir [ip] [esp32,esp32s3] Build, install then run. Optionally specify a platform.")
|
||||
print(" brrr [ip] [esp32,esp32s3] Functionally the same as \"bir\", but \"app goes brrr\" meme variant.")
|
||||
print("")
|
||||
print("Options:")
|
||||
print(" --help Show this commandline info")
|
||||
print(" --local-sdk Use SDK specified by environment variable TACTILITY_SDK_PATH")
|
||||
print(" --skip-build Run everything except the idf.py/CMake commands")
|
||||
print(" --verbose Show extra console output")
|
||||
print(" --help Show this commandline info")
|
||||
print(" --local-sdk Use SDK specified by environment variable TACTILITY_SDK_PATH")
|
||||
print(" --skip-build Run everything except the idf.py/CMake commands")
|
||||
print(" --verbose Show extra console output")
|
||||
|
||||
# region Core
|
||||
|
||||
def download_file(url, filepath):
|
||||
global verbose
|
||||
@ -108,25 +108,19 @@ def exit_with_error(message):
|
||||
def get_url(ip, path):
|
||||
return f"http://{ip}:{ttport}{path}"
|
||||
|
||||
def is_valid_platform_name(name):
|
||||
global platform_arguments
|
||||
return name in platform_arguments
|
||||
def read_properties_file(path):
|
||||
config = configparser.RawConfigParser()
|
||||
config.read(path)
|
||||
return config
|
||||
|
||||
def validate_environment():
|
||||
global ttbuild_properties_file, use_local_sdk
|
||||
if os.environ.get("IDF_PATH") is None:
|
||||
exit_with_error("Cannot find the Espressif IDF SDK. Ensure it is installed and that it is activated via $PATH_TO_IDF_SDK/export.sh")
|
||||
if not os.path.exists(ttbuild_properties_file):
|
||||
exit_with_error(f"{ttbuild_properties_file} file not found")
|
||||
if use_local_sdk == False and os.environ.get("TACTILITY_SDK_PATH") is not None:
|
||||
print_warning("TACTILITY_SDK_PATH is set, but will be ignored by this command.")
|
||||
print_warning("If you want to use it, use the 'build local' parameters.")
|
||||
elif use_local_sdk == True and os.environ.get("TACTILITY_SDK_PATH") is None:
|
||||
exit_with_error("local build was requested, but TACTILITY_SDK_PATH environment variable is not set.")
|
||||
#endregion Core
|
||||
|
||||
def setup_environment():
|
||||
global ttbuild_path
|
||||
os.makedirs(ttbuild_path, exist_ok=True)
|
||||
#region SDK helpers
|
||||
|
||||
def read_sdk_json():
|
||||
json_file_path = os.path.join(ttbuild_path, "sdk.json")
|
||||
json_file = open(json_file_path)
|
||||
return json.load(json_file)
|
||||
|
||||
def get_sdk_dir(version, platform):
|
||||
global use_local_sdk
|
||||
@ -136,15 +130,6 @@ def get_sdk_dir(version, platform):
|
||||
global ttbuild_cdn
|
||||
return os.path.join(ttbuild_path, f"{version}-{platform}", "TactilitySDK")
|
||||
|
||||
def get_sdk_version():
|
||||
global ttbuild_properties_file
|
||||
parser = configparser.RawConfigParser()
|
||||
parser.read(ttbuild_properties_file)
|
||||
sdk_dict = dict(parser.items("sdk"))
|
||||
if not "version" in sdk_dict:
|
||||
exit_with_error(f"Could not find 'version' in [sdk] section in {ttbuild_properties_file}")
|
||||
return sdk_dict["version"]
|
||||
|
||||
def get_sdk_root_dir(version, platform):
|
||||
global ttbuild_cdn
|
||||
return os.path.join(ttbuild_path, f"{version}-{platform}")
|
||||
@ -175,20 +160,34 @@ def update_sdk_json():
|
||||
json_filepath = os.path.join(ttbuild_path, "sdk.json")
|
||||
return download_file(json_url, json_filepath)
|
||||
|
||||
def should_fetch_sdkconfig_files():
|
||||
def should_fetch_sdkconfig_files(platform_targets):
|
||||
for platform in platform_targets:
|
||||
sdkconfig_filename = f"sdkconfig.app.{platform}"
|
||||
if not os.path.exists(os.path.join(ttbuild_path, sdkconfig_filename)):
|
||||
return True
|
||||
return False
|
||||
|
||||
def fetch_sdkconfig_files():
|
||||
def fetch_sdkconfig_files(platform_targets):
|
||||
for platform in platform_targets:
|
||||
sdkconfig_filename = f"sdkconfig.app.{platform}"
|
||||
target_path = os.path.join(ttbuild_path, sdkconfig_filename)
|
||||
if not download_file(f"{ttbuild_cdn}/{sdkconfig_filename}", target_path):
|
||||
exit_with_error(f"Failed to download sdkconfig file for {platform}")
|
||||
|
||||
#endregion SDK helpers
|
||||
|
||||
#region Validation
|
||||
|
||||
def validate_environment():
|
||||
if os.environ.get("IDF_PATH") is None:
|
||||
exit_with_error("Cannot find the Espressif IDF SDK. Ensure it is installed and that it is activated via $PATH_TO_IDF_SDK/export.sh")
|
||||
if not os.path.exists("manifest.properties"):
|
||||
exit_with_error("manifest.properties not found")
|
||||
if use_local_sdk == False and os.environ.get("TACTILITY_SDK_PATH") is not None:
|
||||
print_warning("TACTILITY_SDK_PATH is set, but will be ignored by this command.")
|
||||
print_warning("If you want to use it, use the 'build local' parameters.")
|
||||
elif use_local_sdk == True and os.environ.get("TACTILITY_SDK_PATH") is None:
|
||||
exit_with_error("local build was requested, but TACTILITY_SDK_PATH environment variable is not set.")
|
||||
|
||||
def validate_version_and_platforms(sdk_json, sdk_version, platforms_to_build):
|
||||
version_map = sdk_json["versions"]
|
||||
@ -217,6 +216,64 @@ def validate_self(sdk_json):
|
||||
print_error("Run 'tactility.py updateself' to update.")
|
||||
sys.exit(1)
|
||||
|
||||
#endregion Validation
|
||||
|
||||
#region Manifest
|
||||
|
||||
def read_manifest():
|
||||
return read_properties_file("manifest.properties")
|
||||
|
||||
def validate_manifest(manifest):
|
||||
# [manifest]
|
||||
if not "manifest" in manifest:
|
||||
exit_with_error("Invalid manifest format: [manifest] not found")
|
||||
if not "version" in manifest["manifest"]:
|
||||
exit_with_error("Invalid manifest format: [manifest] version not found")
|
||||
# [target]
|
||||
if not "target" in manifest:
|
||||
exit_with_error("Invalid manifest format: [target] not found")
|
||||
if not "sdk" in manifest["target"]:
|
||||
exit_with_error("Invalid manifest format: [target] sdk not found")
|
||||
if not "platforms" in manifest["target"]:
|
||||
exit_with_error("Invalid manifest format: [target] platforms not found")
|
||||
# [app]
|
||||
if not "app" in manifest:
|
||||
exit_with_error("Invalid manifest format: [app] not found")
|
||||
if not "id" in manifest["app"]:
|
||||
exit_with_error("Invalid manifest format: [app] id not found")
|
||||
if not "version" in manifest["app"]:
|
||||
exit_with_error("Invalid manifest format: [app] version not found")
|
||||
if not "name" in manifest["app"]:
|
||||
exit_with_error("Invalid manifest format: [app] name not found")
|
||||
if not "description" in manifest["app"]:
|
||||
exit_with_error("Invalid manifest format: [app] description not found")
|
||||
# [author]
|
||||
if not "author" in manifest:
|
||||
exit_with_error("Invalid manifest format: [author] not found")
|
||||
if not "name" in manifest["author"]:
|
||||
exit_with_error("Invalid manifest format: [author] name not found")
|
||||
if not "website" in manifest["author"]:
|
||||
exit_with_error("Invalid manifest format: [author] website not found")
|
||||
|
||||
def is_valid_manifest_platform(manifest, platform):
|
||||
manifest_platforms = manifest["target"]["platforms"].split(",")
|
||||
return platform in manifest_platforms
|
||||
|
||||
def validate_manifest_platform(manifest, platform):
|
||||
if not is_valid_manifest_platform(manifest, platform):
|
||||
exit_with_error(f"Platform {platform} is not available in the manifest.")
|
||||
|
||||
def get_manifest_target_platforms(manifest, requested_platform):
|
||||
if requested_platform == "" or requested_platform is None:
|
||||
return manifest["target"]["platforms"].split(",")
|
||||
else:
|
||||
validate_manifest_platform(manifest, requested_platform)
|
||||
return [requested_platform]
|
||||
|
||||
#endregion Manifest
|
||||
|
||||
#region SDK download
|
||||
|
||||
def sdk_download(version, platform):
|
||||
sdk_root_dir = get_sdk_root_dir(version, platform)
|
||||
os.makedirs(sdk_root_dir, exist_ok=True)
|
||||
@ -240,12 +297,19 @@ def sdk_download_all(version, platforms):
|
||||
print(f"Using cached download for SDK version {version} and platform {platform}")
|
||||
return True
|
||||
|
||||
#endregion SDK download
|
||||
|
||||
#region Building
|
||||
|
||||
def get_cmake_path(platform):
|
||||
return os.path.join("build", f"cmake-build-{platform}")
|
||||
|
||||
def find_elf_file(platform):
|
||||
build_dir = f"build-{platform}"
|
||||
if os.path.exists(build_dir):
|
||||
for file in os.listdir(build_dir):
|
||||
cmake_dir = get_cmake_path(platform)
|
||||
if os.path.exists(cmake_dir):
|
||||
for file in os.listdir(cmake_dir):
|
||||
if file.endswith(".app.elf"):
|
||||
return os.path.join(build_dir, file)
|
||||
return os.path.join(cmake_dir, file)
|
||||
return None
|
||||
|
||||
def build_all(version, platforms, skip_build):
|
||||
@ -295,7 +359,8 @@ def build_first(version, platform, skip_build):
|
||||
if skip_build:
|
||||
return True
|
||||
print("Building first build")
|
||||
with subprocess.Popen(["idf.py", "-B", f"build-{platform}", "build"], stdout=subprocess.PIPE, stderr=subprocess.STDOUT) as process:
|
||||
cmake_path = get_cmake_path(platform)
|
||||
with subprocess.Popen(["idf.py", "-B", cmake_path, "build"], stdout=subprocess.PIPE, stderr=subprocess.STDOUT) as process:
|
||||
build_output = wait_for_build(process, platform)
|
||||
# The return code is never expected to be 0 due to a bug in the elf cmake script, but we keep it just in case
|
||||
if process.returncode == 0:
|
||||
@ -320,7 +385,8 @@ def build_consecutively(version, platform, skip_build):
|
||||
os.system(f"cp {sdkconfig_path} sdkconfig")
|
||||
if skip_build:
|
||||
return True
|
||||
with subprocess.Popen(["idf.py", "-B", f"build-{platform}", "elf"], stdout=subprocess.PIPE, stderr=subprocess.STDOUT) as process:
|
||||
cmake_path = get_cmake_path(platform)
|
||||
with subprocess.Popen(["idf.py", "-B", cmake_path, "elf"], stdout=subprocess.PIPE, stderr=subprocess.STDOUT) as process:
|
||||
build_output = wait_for_build(process, platform)
|
||||
if process.returncode == 0:
|
||||
print(f"{shell_color_green}Building for {platform} ✅{shell_color_reset}")
|
||||
@ -331,41 +397,85 @@ def build_consecutively(version, platform, skip_build):
|
||||
print(f"{shell_color_red}Building for {platform} failed ❌{shell_color_reset}")
|
||||
return False
|
||||
|
||||
def read_sdk_json():
|
||||
json_file_path = os.path.join(ttbuild_path, "sdk.json")
|
||||
json_file = open(json_file_path)
|
||||
return json.load(json_file)
|
||||
#endregion Building
|
||||
|
||||
def build_action(platform_arg):
|
||||
#region Packaging
|
||||
|
||||
def package_intermediate_manifest(target_path):
|
||||
if not os.path.isfile("manifest.properties"):
|
||||
print_error("manifest.properties not found")
|
||||
return
|
||||
shutil.copy("manifest.properties", os.path.join(target_path, "manifest.properties"))
|
||||
|
||||
def package_intermediate_binaries(target_path, platforms):
|
||||
elf_dir = os.path.join(target_path, "elf")
|
||||
os.makedirs(elf_dir, exist_ok=True)
|
||||
for platform in platforms:
|
||||
elf_path = find_elf_file(platform)
|
||||
if elf_path is None:
|
||||
print_error(f"ELF file not found at {elf_path}")
|
||||
return
|
||||
shutil.copy(elf_path, os.path.join(elf_dir, f"{platform}.elf"))
|
||||
|
||||
def package_intermediate_assets(target_path):
|
||||
if os.path.isdir("assets"):
|
||||
shutil.copytree("assets", os.path.join(target_path, "assets"), dirs_exist_ok=True)
|
||||
|
||||
def package_intermediate(platforms):
|
||||
target_path = os.path.join("build", "package-intermediate")
|
||||
if os.path.isdir(target_path):
|
||||
shutil.rmtree(target_path)
|
||||
os.makedirs(target_path, exist_ok=True)
|
||||
package_intermediate_manifest(target_path)
|
||||
package_intermediate_binaries(target_path, platforms)
|
||||
package_intermediate_assets(target_path)
|
||||
|
||||
def package_name(platforms):
|
||||
elf_path = find_elf_file(platforms[0])
|
||||
elf_base_name = os.path.basename(elf_path).removesuffix(".app.elf")
|
||||
return os.path.join("build", f"{elf_base_name}.app")
|
||||
|
||||
def package_all(platforms):
|
||||
print("Packaging app")
|
||||
package_intermediate(platforms)
|
||||
# Create build/something.app
|
||||
tar_path = package_name(platforms)
|
||||
tar = tarfile.open(tar_path, mode="w", format=tarfile.USTAR_FORMAT)
|
||||
tar.add(os.path.join("build", "package-intermediate"), arcname="")
|
||||
tar.close()
|
||||
|
||||
#endregion Packaging
|
||||
|
||||
def setup_environment():
|
||||
global ttbuild_path
|
||||
os.makedirs(ttbuild_path, exist_ok=True)
|
||||
|
||||
def build_action(manifest, platform_arg):
|
||||
# Environment validation
|
||||
validate_environment()
|
||||
platforms_to_build = platform_targets if platform_arg == "all" else [platform_arg]
|
||||
if not is_valid_platform_name(platform_arg):
|
||||
print_help()
|
||||
exit_with_error("Invalid platform name")
|
||||
platforms_to_build = get_manifest_target_platforms(manifest, platform_arg)
|
||||
if not use_local_sdk:
|
||||
if should_fetch_sdkconfig_files():
|
||||
fetch_sdkconfig_files()
|
||||
if should_fetch_sdkconfig_files(platforms_to_build):
|
||||
fetch_sdkconfig_files(platforms_to_build)
|
||||
sdk_json = read_sdk_json()
|
||||
validate_self(sdk_json)
|
||||
if not "versions" in sdk_json:
|
||||
exit_with_error("Version data not found in sdk.json")
|
||||
# Build
|
||||
sdk_version = get_sdk_version()
|
||||
sdk_version = manifest["target"]["sdk"]
|
||||
if not use_local_sdk:
|
||||
validate_version_and_platforms(sdk_json, sdk_version, platforms_to_build)
|
||||
if not sdk_download_all(sdk_version, platforms_to_build):
|
||||
exit_with_error("Failed to download one or more SDKs")
|
||||
build_all(sdk_version, platforms_to_build, skip_build) # Environment validation
|
||||
if not skip_build:
|
||||
package_all(platforms_to_build)
|
||||
|
||||
def clean_action():
|
||||
count = 0
|
||||
for path in os.listdir("."):
|
||||
if path.startswith("build-"):
|
||||
print(f"Removing {path}/")
|
||||
shutil.rmtree(path)
|
||||
count = count + 1
|
||||
if count == 0:
|
||||
if os.path.exists("build"):
|
||||
print(f"Removing build/")
|
||||
shutil.rmtree("build")
|
||||
else:
|
||||
print("Nothing to clean")
|
||||
|
||||
def clear_cache_action():
|
||||
@ -396,7 +506,8 @@ def get_device_info(ip):
|
||||
except requests.RequestException as e:
|
||||
print(f"Request failed: {e}")
|
||||
|
||||
def run_action(ip, app_id):
|
||||
def run_action(manifest, ip):
|
||||
app_id = manifest["app"]["id"]
|
||||
print(f"Running {app_id} on {ip}")
|
||||
url = get_url(ip, "/app/run")
|
||||
params = {'id': app_id}
|
||||
@ -409,16 +520,17 @@ def run_action(ip, app_id):
|
||||
except requests.RequestException as e:
|
||||
print(f"Request failed: {e}")
|
||||
|
||||
def install_action(ip, platform):
|
||||
file_path = find_elf_file(platform)
|
||||
if file_path is None:
|
||||
print_error(f"File not found: {file_path}")
|
||||
return
|
||||
print(f"Installing {file_path} to {ip}")
|
||||
def install_action(ip, platforms):
|
||||
for platform in platforms:
|
||||
elf_path = find_elf_file(platform)
|
||||
if elf_path is None:
|
||||
exit_with_error(f"ELF file not built for {platform}")
|
||||
package_path = package_name(platforms)
|
||||
print(f"Installing {package_path} to {ip}")
|
||||
url = get_url(ip, "/app/install")
|
||||
try:
|
||||
# Prepare multipart form data
|
||||
with open(file_path, 'rb') as file:
|
||||
with open(package_path, 'rb') as file:
|
||||
files = {
|
||||
'elf': file
|
||||
}
|
||||
@ -432,6 +544,7 @@ def install_action(ip, platform):
|
||||
except IOError as e:
|
||||
print_error(f"File error: {e}")
|
||||
|
||||
#region Main
|
||||
|
||||
if __name__ == "__main__":
|
||||
print(f"Tactility Build System v{ttbuild_version}")
|
||||
@ -448,15 +561,23 @@ if __name__ == "__main__":
|
||||
use_local_sdk = "--local-sdk" in sys.argv
|
||||
# Environment setup
|
||||
setup_environment()
|
||||
if not os.path.isfile("manifest.properties"):
|
||||
exit_with_error("manifest.properties not found")
|
||||
manifest = read_manifest()
|
||||
validate_manifest(manifest)
|
||||
all_platform_targets = manifest["target"]["platforms"].split(",")
|
||||
# Update SDK cache (sdk.json)
|
||||
if should_update_sdk_json() and not update_sdk_json():
|
||||
exit_with_error("Failed to retrieve SDK info")
|
||||
# Actions
|
||||
if action_arg == "build":
|
||||
if len(sys.argv) < 3:
|
||||
if len(sys.argv) < 2:
|
||||
print_help()
|
||||
exit_with_error("Commandline parameter missing")
|
||||
build_action(sys.argv[2])
|
||||
platform = None
|
||||
if len(sys.argv) > 2:
|
||||
platform = sys.argv[2]
|
||||
build_action(manifest, platform)
|
||||
elif action_arg == "clean":
|
||||
clean_action()
|
||||
elif action_arg == "clearcache":
|
||||
@ -464,15 +585,34 @@ if __name__ == "__main__":
|
||||
elif action_arg == "updateself":
|
||||
update_self_action()
|
||||
elif action_arg == "run":
|
||||
if len(sys.argv) < 4:
|
||||
if len(sys.argv) < 3:
|
||||
print_help()
|
||||
exit_with_error("Commandline parameter missing")
|
||||
run_action(sys.argv[2], sys.argv[3])
|
||||
run_action(manifest, sys.argv[2])
|
||||
elif action_arg == "install":
|
||||
if len(sys.argv) < 4:
|
||||
if len(sys.argv) < 3:
|
||||
print_help()
|
||||
exit_with_error("Commandline parameter missing")
|
||||
install_action(sys.argv[2], sys.argv[3])
|
||||
platform = None
|
||||
platforms_to_install = all_platform_targets
|
||||
if len(sys.argv) >= 4:
|
||||
platform = sys.argv[3]
|
||||
platforms_to_install = [platform]
|
||||
install_action(sys.argv[2], platforms_to_install)
|
||||
elif action_arg == "bir":
|
||||
if len(sys.argv) < 3:
|
||||
print_help()
|
||||
exit_with_error("Commandline parameter missing")
|
||||
platform = None
|
||||
platforms_to_install = all_platform_targets
|
||||
if len(sys.argv) >= 4:
|
||||
platform = sys.argv[3]
|
||||
platforms_to_install = [platform]
|
||||
build_action(manifest, platform)
|
||||
install_action(sys.argv[2], platforms_to_install)
|
||||
run_action(manifest, sys.argv[2])
|
||||
else:
|
||||
print_help()
|
||||
exit_with_error("Unknown commandline parameter")
|
||||
|
||||
#endregion Main
|
||||
|
||||
22
Libraries/minitar/CMakeLists.txt
Normal file
22
Libraries/minitar/CMakeLists.txt
Normal file
@ -0,0 +1,22 @@
|
||||
cmake_minimum_required(VERSION 3.20)
|
||||
|
||||
set(C_STANDARD 11)
|
||||
set(CC_STANDARD_REQUIRED ON)
|
||||
|
||||
if (DEFINED ENV{ESP_IDF_VERSION})
|
||||
idf_component_register(
|
||||
SRC_DIRS "minitar/src/"
|
||||
INCLUDE_DIRS "minitar/"
|
||||
)
|
||||
|
||||
else()
|
||||
file(GLOB SOURCES "minitar/src/*.*")
|
||||
file(GLOB HEADERS "minitar/*.h")
|
||||
|
||||
add_library(minitar STATIC)
|
||||
|
||||
target_sources(minitar PRIVATE ${SOURCES})
|
||||
include_directories(minitar "minitar/src/")
|
||||
target_include_directories(minitar PUBLIC "minitar/")
|
||||
|
||||
endif()
|
||||
1
Libraries/minitar/minitar
Submodule
1
Libraries/minitar/minitar
Submodule
@ -0,0 +1 @@
|
||||
Subproject commit 78c254ba114f6b66d888149d4ad0eff178dceb88
|
||||
@ -6,7 +6,7 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
if (DEFINED ENV{ESP_IDF_VERSION})
|
||||
file(GLOB_RECURSE SOURCE_FILES Source/*.c*)
|
||||
|
||||
list(APPEND REQUIRES_LIST TactilityCore lvgl driver elf_loader lv_screenshot QRCode esp_lvgl_port minmea esp_wifi nvs_flash spiffs vfs fatfs lwip esp_http_server)
|
||||
list(APPEND REQUIRES_LIST TactilityCore lvgl driver elf_loader lv_screenshot QRCode esp_lvgl_port minitar minmea esp_wifi nvs_flash spiffs vfs fatfs lwip esp_http_server)
|
||||
if ("${IDF_TARGET}" STREQUAL "esp32s3")
|
||||
list(APPEND REQUIRES_LIST esp_tinyusb)
|
||||
endif ()
|
||||
@ -55,6 +55,7 @@ else()
|
||||
PUBLIC lvgl
|
||||
PUBLIC lv_screenshot
|
||||
PUBLIC minmea
|
||||
PUBLIC minitar
|
||||
)
|
||||
endif()
|
||||
|
||||
|
||||
@ -94,4 +94,10 @@ std::shared_ptr<AppContext> _Nullable getCurrentAppContext();
|
||||
/** @return the currently running app (it is only ever null before the splash screen is shown) */
|
||||
std::shared_ptr<App> _Nullable getCurrentApp();
|
||||
|
||||
std::string getTempPath();
|
||||
|
||||
std::string getInstallPath();
|
||||
|
||||
bool install(const std::string& path);
|
||||
|
||||
}
|
||||
|
||||
@ -26,13 +26,6 @@ void setElfAppManifest(
|
||||
OnResult _Nullable onResult
|
||||
);
|
||||
|
||||
/**
|
||||
* @return the app ID based on the executable's file path.
|
||||
*/
|
||||
std::string getElfAppId(const std::string& filePath);
|
||||
|
||||
void registerElfApp(const std::string& filePath);
|
||||
|
||||
std::shared_ptr<App> createElfApp(const std::shared_ptr<AppManifest>& manifest);
|
||||
|
||||
}
|
||||
|
||||
@ -4,6 +4,7 @@
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <Tactility/app/App.h>
|
||||
|
||||
/**
|
||||
* Start the app by its ID and provide:
|
||||
@ -18,15 +19,17 @@ namespace tt::app::alertdialog {
|
||||
* @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
|
||||
*/
|
||||
void start(const std::string& title, const std::string& message, const std::vector<std::string>& buttonLabels);
|
||||
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 an OK button
|
||||
* @param[in] title the title to show in the toolbar
|
||||
* @param[in] message the message to display
|
||||
* @return the launch id
|
||||
*/
|
||||
void start(const std::string& title, const std::string& message);
|
||||
LaunchId start(const std::string& title, const std::string& message);
|
||||
|
||||
/**
|
||||
* Get the index of the button that the user selected.
|
||||
|
||||
@ -21,7 +21,7 @@ public:
|
||||
Service() = default;
|
||||
virtual ~Service() = default;
|
||||
|
||||
virtual void onStart(ServiceContext& serviceContext) {}
|
||||
virtual bool onStart(ServiceContext& serviceContext) { return true; }
|
||||
virtual void onStop(ServiceContext& serviceContext) {}
|
||||
};
|
||||
|
||||
|
||||
@ -31,7 +31,7 @@ struct EspNowConfig {
|
||||
bool longRange,
|
||||
bool encrypt
|
||||
) : mode(mode), channel(channel), longRange(longRange), encrypt(encrypt) {
|
||||
memcpy((void*)this->masterKey, (void*)masterKey, 16);
|
||||
memcpy(this->masterKey, masterKey, 16);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@ -44,7 +44,7 @@ class GpsService final : public Service {
|
||||
|
||||
public:
|
||||
|
||||
void onStart(ServiceContext &serviceContext) override;
|
||||
bool onStart(ServiceContext &serviceContext) override;
|
||||
void onStop(ServiceContext &serviceContext) override;
|
||||
|
||||
bool addGpsConfiguration(hal::gps::GpsConfiguration configuration);
|
||||
|
||||
@ -24,6 +24,7 @@ enum class LoaderEvent{
|
||||
* @brief Start an app
|
||||
* @param[in] id application name or id
|
||||
* @param[in] parameters optional parameters to pass onto the application
|
||||
* @return the launch id
|
||||
*/
|
||||
app::LaunchId startApp(const std::string& id, std::shared_ptr<const Bundle> _Nullable parameters = nullptr);
|
||||
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
|
||||
namespace tt::app::filebrowser {
|
||||
|
||||
bool isSupportedExecutableFile(const std::string& filename);
|
||||
bool isSupportedAppFile(const std::string& filename);
|
||||
bool isSupportedImageFile(const std::string& filename);
|
||||
bool isSupportedTextFile(const std::string& filename);
|
||||
|
||||
|
||||
@ -16,6 +16,9 @@ class View {
|
||||
lv_obj_t* action_list = nullptr;
|
||||
lv_obj_t* navigate_up_button = nullptr;
|
||||
|
||||
std::string installAppPath = { 0 };
|
||||
LaunchId installAppLaunchId = 0;
|
||||
|
||||
void showActionsForDirectory();
|
||||
void showActionsForFile();
|
||||
|
||||
@ -36,7 +39,7 @@ public:
|
||||
void onRenamePressed();
|
||||
void onDeletePressed();
|
||||
void onDirEntryListScrollBegin();
|
||||
void onResult(Result result, std::unique_ptr<Bundle> bundle);
|
||||
void onResult(LaunchId launchId, Result result, std::unique_ptr<Bundle> bundle);
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
@ -55,7 +55,7 @@ public:
|
||||
|
||||
// region Overrides
|
||||
|
||||
void onStart(ServiceContext& service) override;
|
||||
bool onStart(ServiceContext& service) override;
|
||||
void onStop(ServiceContext& service) override;
|
||||
|
||||
// endregion Overrides
|
||||
|
||||
@ -42,7 +42,7 @@ public:
|
||||
|
||||
// region Overrides
|
||||
|
||||
void onStart(ServiceContext& service) override;
|
||||
bool onStart(ServiceContext& service) override;
|
||||
void onStop(ServiceContext& service) override;
|
||||
|
||||
// endregion Overrides
|
||||
|
||||
@ -52,18 +52,17 @@ class GuiService : public Service {
|
||||
tt_check(mutex.unlock());
|
||||
}
|
||||
|
||||
public:
|
||||
|
||||
void onStart(TT_UNUSED ServiceContext& service) override;
|
||||
|
||||
void onStop(TT_UNUSED ServiceContext& service) override;
|
||||
|
||||
void requestDraw();
|
||||
|
||||
void showApp(std::shared_ptr<app::AppContext> app);
|
||||
|
||||
void hideApp();
|
||||
|
||||
public:
|
||||
|
||||
bool onStart(TT_UNUSED ServiceContext& service) override;
|
||||
|
||||
void onStop(TT_UNUSED ServiceContext& service) override;
|
||||
|
||||
void requestDraw();
|
||||
|
||||
/**
|
||||
* Show the on-screen keyboard.
|
||||
|
||||
@ -19,14 +19,14 @@ enum class Mode {
|
||||
|
||||
class ScreenshotService final : public Service {
|
||||
|
||||
private:
|
||||
|
||||
Mutex mutex;
|
||||
std::unique_ptr<ScreenshotTask> task;
|
||||
Mode mode = Mode::None;
|
||||
|
||||
public:
|
||||
|
||||
bool onStart(ServiceContext& serviceContext) override;
|
||||
|
||||
bool isTaskStarted();
|
||||
|
||||
/** The state of the service. */
|
||||
|
||||
@ -1,10 +1,11 @@
|
||||
#include <Tactility/Tactility.h>
|
||||
#include <Tactility/TactilityConfig.h>
|
||||
|
||||
#include <Tactility/app/AppRegistration.h>
|
||||
#include <Tactility/DispatcherThread.h>
|
||||
#include <Tactility/MountPoints.h>
|
||||
#include <Tactility/file/File.h>
|
||||
#include <Tactility/file/PropertiesFile.h>
|
||||
#include <Tactility/hal/HalPrivate.h>
|
||||
#include <Tactility/hal/sdcard/SdCardMounting.h>
|
||||
#include <Tactility/lvgl/LvglPrivate.h>
|
||||
#include <Tactility/network/NtpPrivate.h>
|
||||
#include <Tactility/service/ServiceManifest.h>
|
||||
@ -12,6 +13,9 @@
|
||||
#include <Tactility/service/loader/Loader.h>
|
||||
#include <Tactility/settings/TimePrivate.h>
|
||||
|
||||
#include <map>
|
||||
#include <format>
|
||||
|
||||
#ifdef ESP_PLATFORM
|
||||
#include <Tactility/InitEsp.h>
|
||||
#endif
|
||||
@ -137,6 +141,67 @@ static void registerSystemApps() {
|
||||
}
|
||||
}
|
||||
|
||||
static void registerInstalledApp(std::string path) {
|
||||
TT_LOG_I(TAG, "Registering app at %s", path.c_str());
|
||||
std::string manifest_path = path + "/manifest.properties";
|
||||
if (!file::isFile(manifest_path)) {
|
||||
TT_LOG_E(TAG, "Manifest not found at %s", manifest_path.c_str());
|
||||
return;
|
||||
}
|
||||
|
||||
std::map<std::string, std::string> manifest;
|
||||
if (!file::loadPropertiesFile(manifest_path, manifest)) {
|
||||
TT_LOG_E(TAG, "Failed to load manifest at %s", manifest_path.c_str());
|
||||
}
|
||||
|
||||
auto app_id_entry = manifest.find("[app]id");
|
||||
if (app_id_entry == manifest.end()) {
|
||||
TT_LOG_E(TAG, "Failed to find app id in manifest");
|
||||
return;
|
||||
}
|
||||
|
||||
auto app_name_entry = manifest.find("[app]name");
|
||||
if (app_name_entry == manifest.end()) {
|
||||
TT_LOG_E(TAG, "Failed to find app name in manifest");
|
||||
return;
|
||||
}
|
||||
|
||||
app::addApp({
|
||||
.id = app_id_entry->second,
|
||||
.name = app_name_entry->second,
|
||||
.type = app::Type::User,
|
||||
.location = app::Location::external(path)
|
||||
});
|
||||
}
|
||||
|
||||
static void registerInstalledApps(const std::string& path) {
|
||||
file::listDirectory(path, [&path](const auto& entry) {
|
||||
auto absolute_path = std::format("{}/{}", path, entry.d_name);
|
||||
if (file::isDirectory(absolute_path)) {
|
||||
registerInstalledApp(absolute_path);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
static void registerInstalledAppsFromSdCard(const std::shared_ptr<hal::sdcard::SdCardDevice>& sdcard) {
|
||||
auto sdcard_root_path = sdcard->getMountPath();
|
||||
auto app_path = std::format("{}/apps", sdcard_root_path);
|
||||
sdcard->getLock()->lock();
|
||||
if (file::isDirectory(app_path)) {
|
||||
registerInstalledApps(app_path);
|
||||
}
|
||||
sdcard->getLock()->unlock();
|
||||
}
|
||||
|
||||
static void registerInstalledAppsFromSdCards() {
|
||||
auto sdcard_devices = hal::findDevices<hal::sdcard::SdCardDevice>(hal::Device::Type::SdCard);
|
||||
for (const auto& sdcard : sdcard_devices) {
|
||||
if (sdcard->isMounted()) {
|
||||
registerInstalledAppsFromSdCard(sdcard);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void registerUserApps(const std::vector<const app::AppManifest*>& apps) {
|
||||
TT_LOG_I(TAG, "Registering user apps");
|
||||
for (auto* manifest : apps) {
|
||||
@ -176,8 +241,13 @@ static void registerAndStartUserServices(const std::vector<const service::Servic
|
||||
|
||||
void initFromBootApp() {
|
||||
auto configuration = getConfiguration();
|
||||
// Then we register system apps. They are not used/started yet.
|
||||
// Then we register apps. They are not used/started yet.
|
||||
registerSystemApps();
|
||||
auto data_apps_path = std::format("{}/apps", file::MOUNT_POINT_DATA);
|
||||
if (file::isDirectory(data_apps_path)) {
|
||||
registerInstalledApps(data_apps_path);
|
||||
}
|
||||
registerInstalledAppsFromSdCards();
|
||||
// Then we register and start user services. They are started after system app
|
||||
// registration just in case they want to figure out which system apps are installed.
|
||||
registerAndStartUserServices(configuration->services);
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
#include "Tactility/app/App.h"
|
||||
|
||||
#include <Tactility/app/App.h>
|
||||
#include <Tactility/service/loader/Loader.h>
|
||||
|
||||
namespace tt::app {
|
||||
|
||||
226
Tactility/Source/app/AppInstall.cpp
Normal file
226
Tactility/Source/app/AppInstall.cpp
Normal file
@ -0,0 +1,226 @@
|
||||
#include <Tactility/app/App.h>
|
||||
|
||||
#include <Tactility/MountPoints.h>
|
||||
#include <Tactility/app/AppManifest.h>
|
||||
#include <Tactility/app/AppRegistration.h>
|
||||
#include <Tactility/file/File.h>
|
||||
#include <Tactility/file/FileLock.h>
|
||||
#include <Tactility/file/PropertiesFile.h>
|
||||
#include <Tactility/hal/Device.h>
|
||||
#include <Tactility/hal/sdcard/SdCardDevice.h>
|
||||
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
#include <format>
|
||||
#include <libgen.h>
|
||||
#include <map>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/types.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <minitar.h>
|
||||
|
||||
constexpr auto* TAG = "App";
|
||||
|
||||
namespace tt::app {
|
||||
|
||||
static int untarFile(const minitar_entry* entry, const void* buf, const std::string& destinationPath) {
|
||||
auto absolute_path = destinationPath + "/" + entry->metadata.path;
|
||||
if (!file::findOrCreateDirectory(destinationPath, 0777)) return 1;
|
||||
|
||||
int fd = open(absolute_path.c_str(), O_WRONLY | O_CREAT | O_EXCL | O_CLOEXEC, 0644);
|
||||
if (fd < 0) return 1;
|
||||
|
||||
if (write(fd, buf, entry->metadata.size) < 0) return 1;
|
||||
|
||||
// Note: fchmod() doesn't exist on ESP-IDF and chmod() does nothing on that platform
|
||||
if (chmod(absolute_path.c_str(), entry->metadata.mode) < 0) return 1;
|
||||
|
||||
return close(fd);
|
||||
}
|
||||
|
||||
static bool untar_directory(const minitar_entry* entry, const std::string& destinationPath) {
|
||||
auto absolute_path = destinationPath + "/" + entry->metadata.path;
|
||||
if (!file::findOrCreateDirectory(absolute_path, 0777)) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool untar(const std::string& tarPath, const std::string& destinationPath) {
|
||||
minitar mp;
|
||||
if (minitar_open(tarPath.c_str(), &mp) != 0) {
|
||||
perror(tarPath.c_str());
|
||||
return 1;
|
||||
}
|
||||
bool success = true;
|
||||
minitar_entry entry;
|
||||
|
||||
do {
|
||||
if (minitar_read_entry(&mp, &entry) == 0) {
|
||||
TT_LOG_I(TAG, "Extracting %s", entry.metadata.path);
|
||||
if (entry.metadata.type == MTAR_DIRECTORY) {
|
||||
if (!strcmp(entry.metadata.name, ".") || !strcmp(entry.metadata.name, "..") || !strcmp(entry.metadata.name, "/")) continue;
|
||||
if (!untar_directory(&entry, destinationPath)) {
|
||||
TT_LOG_E(TAG, "Failed to create directory %s/%s: %s", destinationPath.c_str(), entry.metadata.name, strerror(errno));
|
||||
success = false;
|
||||
break;
|
||||
}
|
||||
} else if (entry.metadata.type == MTAR_REGULAR) {
|
||||
auto file_buffer = static_cast<char*>(malloc(entry.metadata.size));
|
||||
if (!file_buffer) {
|
||||
TT_LOG_E(TAG, "Failed to allocate %d bytes for file %s", entry.metadata.size, entry.metadata.path);;
|
||||
success = false;
|
||||
break;
|
||||
}
|
||||
|
||||
minitar_read_contents(&mp, &entry, file_buffer, entry.metadata.size);
|
||||
int status = untarFile(&entry, file_buffer, destinationPath);
|
||||
free(file_buffer);
|
||||
if (status != 0) {
|
||||
TT_LOG_E(TAG, "Failed to extract file %s: %s", entry.metadata.path, strerror(errno));
|
||||
success = false;
|
||||
break;
|
||||
}
|
||||
} else if (entry.metadata.type == MTAR_SYMLINK) {
|
||||
TT_LOG_E(TAG, "SYMLINK not supported");
|
||||
} else if (entry.metadata.type == MTAR_HARDLINK) {
|
||||
TT_LOG_E(TAG, "HARDLINK not supported");
|
||||
} else if (entry.metadata.type == MTAR_FIFO) {
|
||||
TT_LOG_E(TAG, "FIFO not supported");
|
||||
} else if (entry.metadata.type == MTAR_BLKDEV) {
|
||||
TT_LOG_E(TAG, "BLKDEV not supported");
|
||||
} else if (entry.metadata.type == MTAR_CHRDEV) {
|
||||
TT_LOG_E(TAG, "CHRDEV not supported");
|
||||
} else {
|
||||
TT_LOG_E(TAG, "Unknown entry type: %d", entry.metadata.type);
|
||||
success = false;
|
||||
break;
|
||||
}
|
||||
} else break;
|
||||
} while (true);
|
||||
minitar_close(&mp);
|
||||
return success;
|
||||
}
|
||||
|
||||
bool findFirstMountedSdCardPath(std::string& path) {
|
||||
// const auto sdcards = hal::findDevices<hal::sdcard::SdCardDevice>(hal::Device::Type::SdCard);
|
||||
bool is_set = false;
|
||||
hal::findDevices<hal::sdcard::SdCardDevice>(hal::Device::Type::SdCard, [&is_set, &path](const auto& device) {
|
||||
if (device->isMounted()) {
|
||||
path = device->getMountPath();
|
||||
is_set = true;
|
||||
return false; // stop iterating
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
});
|
||||
return is_set;
|
||||
}
|
||||
|
||||
std::string getTempPath() {
|
||||
std::string root_path;
|
||||
if (!findFirstMountedSdCardPath(root_path)) {
|
||||
root_path = file::MOUNT_POINT_DATA;
|
||||
}
|
||||
return root_path + "/tmp";
|
||||
}
|
||||
|
||||
std::string getInstallPath() {
|
||||
std::string root_path;
|
||||
if (!findFirstMountedSdCardPath(root_path)) {
|
||||
root_path = file::MOUNT_POINT_DATA;
|
||||
}
|
||||
return root_path + "/apps";
|
||||
}
|
||||
|
||||
bool install(const std::string& path) {
|
||||
// TODO: Make better: lock for each path type properly (source vs target)
|
||||
|
||||
// We lock and unlock frequently because SPI SD card devices share
|
||||
// the lock with the display. We don't want to lock the display for very long.
|
||||
|
||||
auto app_parent_path = getInstallPath();
|
||||
TT_LOG_I(TAG, "Installing app %s to %s", path.c_str(), app_parent_path.c_str());
|
||||
|
||||
auto lock = file::getLock(app_parent_path)->asScopedLock();
|
||||
|
||||
lock.lock();
|
||||
auto filename = file::getLastPathSegment(path);
|
||||
const std::string app_target_path = std::format("{}/{}", app_parent_path, filename);
|
||||
if (file::isDirectory(app_target_path) && !file::deleteRecursively(app_target_path)) {
|
||||
TT_LOG_W(TAG, "Failed to delete %s", app_target_path.c_str());
|
||||
}
|
||||
lock.unlock();
|
||||
|
||||
lock.lock();
|
||||
if (!file::findOrCreateDirectory(app_target_path, 0777)) {
|
||||
TT_LOG_I(TAG, "Failed to create directory %s", app_target_path.c_str());
|
||||
return false;
|
||||
}
|
||||
lock.unlock();
|
||||
|
||||
lock.lock();
|
||||
TT_LOG_I(TAG, "Extracting app from %s to %s", path.c_str(), app_target_path.c_str());
|
||||
if (!untar(path, app_target_path)) {
|
||||
TT_LOG_E(TAG, "Failed to extract");
|
||||
return false;
|
||||
}
|
||||
lock.unlock();
|
||||
|
||||
lock.lock();
|
||||
auto manifest_path = app_target_path + "/manifest.properties";
|
||||
if (!file::isFile(manifest_path)) {
|
||||
TT_LOG_E(TAG, "Manifest not found at %s", manifest_path.c_str());
|
||||
return false;
|
||||
}
|
||||
lock.unlock();
|
||||
|
||||
lock.lock();
|
||||
std::map<std::string, std::string> properties;
|
||||
if (!file::loadPropertiesFile(manifest_path, properties)) {
|
||||
TT_LOG_E(TAG, "Failed to load manifest at %s", manifest_path.c_str());
|
||||
return false;
|
||||
}
|
||||
lock.unlock();
|
||||
|
||||
auto app_id_iterator = properties.find("[app]id");
|
||||
if (app_id_iterator == properties.end()) {
|
||||
TT_LOG_E(TAG, "Failed to find app id in manifest");
|
||||
return false;
|
||||
}
|
||||
|
||||
auto app_name_entry = properties.find("[app]name");
|
||||
if (app_name_entry == properties.end()) {
|
||||
TT_LOG_E(TAG, "Failed to find app name in manifest");
|
||||
return false;
|
||||
}
|
||||
|
||||
lock.lock();
|
||||
const std::string renamed_target_path = std::format("{}/{}", app_parent_path, app_id_iterator->second);
|
||||
if (file::isDirectory(renamed_target_path)) {
|
||||
if (!file::deleteRecursively(renamed_target_path)) {
|
||||
TT_LOG_W(TAG, "Failed to delete existing installation at %s", renamed_target_path.c_str());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
lock.unlock();
|
||||
|
||||
lock.lock();
|
||||
if (rename(app_target_path.c_str(), renamed_target_path.c_str()) != 0) {
|
||||
TT_LOG_E(TAG, "Failed to rename %s to %s", app_target_path.c_str(), app_id_iterator->second.c_str());
|
||||
return false;
|
||||
}
|
||||
lock.unlock();
|
||||
|
||||
addApp({
|
||||
.id = app_id_iterator->second,
|
||||
.name = app_name_entry->second,
|
||||
.type = Type::User,
|
||||
.location = Location::external(renamed_target_path)
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
@ -20,12 +20,12 @@ void addApp(const AppManifest& manifest) {
|
||||
|
||||
hash_mutex.lock();
|
||||
|
||||
if (!app_manifest_map.contains(manifest.id)) {
|
||||
app_manifest_map[manifest.id] = std::make_shared<AppManifest>(manifest);
|
||||
} else {
|
||||
TT_LOG_E(TAG, "App id in use: %s", manifest.id.c_str());
|
||||
if (app_manifest_map.contains(manifest.id)) {
|
||||
TT_LOG_W(TAG, "Overwriting existing manifest for %s", manifest.id.c_str());
|
||||
}
|
||||
|
||||
app_manifest_map[manifest.id] = std::make_shared<AppManifest>(manifest);
|
||||
|
||||
hash_mutex.unlock();
|
||||
}
|
||||
|
||||
|
||||
@ -38,7 +38,7 @@ static std::shared_ptr<Lock> elfManifestLock = std::make_shared<Mutex>();
|
||||
|
||||
class ElfApp : public App {
|
||||
|
||||
const std::string filePath;
|
||||
const std::string appPath;
|
||||
std::unique_ptr<uint8_t[]> elfFileData;
|
||||
esp_elf_t elf {
|
||||
.psegment = nullptr,
|
||||
@ -54,12 +54,13 @@ class ElfApp : public App {
|
||||
std::string lastError = "";
|
||||
|
||||
bool startElf() {
|
||||
TT_LOG_I(TAG, "Starting ELF %s", filePath.c_str());
|
||||
const std::string elf_path = std::format("{}/elf/{}.elf", appPath, CONFIG_IDF_TARGET);
|
||||
TT_LOG_I(TAG, "Starting ELF %s", elf_path.c_str());
|
||||
assert(elfFileData == nullptr);
|
||||
|
||||
size_t size = 0;
|
||||
file::withLock<void>(filePath, [this, &size]{
|
||||
elfFileData = file::readBinary(filePath, size);
|
||||
file::withLock<void>(elf_path, [this, &elf_path, &size]{
|
||||
elfFileData = file::readBinary(elf_path, size);
|
||||
});
|
||||
|
||||
if (elfFileData == nullptr) {
|
||||
@ -111,7 +112,7 @@ class ElfApp : public App {
|
||||
|
||||
public:
|
||||
|
||||
explicit ElfApp(std::string filePath) : filePath(std::move(filePath)) {}
|
||||
explicit ElfApp(std::string appPath) : appPath(std::move(appPath)) {}
|
||||
|
||||
void onCreate(AppContext& appContext) override {
|
||||
// Because we use global variables, we have to ensure that we are not starting 2 apps in parallel
|
||||
@ -205,22 +206,6 @@ void setElfAppManifest(
|
||||
elfManifestSetCount++;
|
||||
}
|
||||
|
||||
std::string getElfAppId(const std::string& filePath) {
|
||||
return filePath;
|
||||
}
|
||||
|
||||
void registerElfApp(const std::string& filePath) {
|
||||
if (findAppById(filePath) == nullptr) {
|
||||
auto manifest = AppManifest {
|
||||
.id = getElfAppId(filePath),
|
||||
.name = string::removeFileExtension(string::getLastPathSegment(filePath)),
|
||||
.type = Type::User,
|
||||
.location = Location::external(filePath)
|
||||
};
|
||||
addApp(manifest);
|
||||
}
|
||||
}
|
||||
|
||||
std::shared_ptr<App> createElfApp(const std::shared_ptr<AppManifest>& manifest) {
|
||||
TT_LOG_I(TAG, "createElfApp");
|
||||
assert(manifest != nullptr);
|
||||
|
||||
@ -74,6 +74,7 @@ public:
|
||||
|
||||
void onShow(AppContext& app, lv_obj_t* parent) final {
|
||||
lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN);
|
||||
lv_obj_set_style_pad_row(parent, 0, LV_STATE_DEFAULT);
|
||||
|
||||
lvgl::toolbar_create(parent, app);
|
||||
|
||||
|
||||
@ -22,21 +22,21 @@ namespace tt::app::alertdialog {
|
||||
|
||||
extern const AppManifest manifest;
|
||||
|
||||
void start(const std::string& title, const std::string& message, const std::vector<std::string>& buttonLabels) {
|
||||
LaunchId start(const std::string& title, const std::string& message, const std::vector<std::string>& 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);
|
||||
service::loader::startApp(manifest.id, bundle);
|
||||
return service::loader::startApp(manifest.id, bundle);
|
||||
}
|
||||
|
||||
void start(const std::string& title, const std::string& message) {
|
||||
LaunchId start(const std::string& title, const std::string& message) {
|
||||
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, "OK");
|
||||
service::loader::startApp(manifest.id, bundle);
|
||||
return service::loader::startApp(manifest.id, bundle);
|
||||
}
|
||||
|
||||
int32_t getResultIndex(const Bundle& bundle) {
|
||||
|
||||
@ -39,7 +39,9 @@ class BootApp : public App {
|
||||
|
||||
static void setupDisplay() {
|
||||
const auto hal_display = getHalDisplay();
|
||||
assert(hal_display != nullptr);
|
||||
if (hal_display == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
settings::display::DisplaySettings settings;
|
||||
if (settings::display::load(settings)) {
|
||||
@ -81,13 +83,27 @@ class BootApp : public App {
|
||||
}
|
||||
|
||||
static int32_t bootThreadCallback() {
|
||||
TT_LOG_I(TAG, "Starting boot thread");
|
||||
const auto start_time = kernel::getTicks();
|
||||
|
||||
kernel::publishSystemEvent(kernel::SystemEvent::BootSplash);
|
||||
// Give the UI some time to redraw
|
||||
// If we don't do this, various init calls will read files and block SPI IO for the display
|
||||
// This would result in a blank/black screen being shown during this phase of the boot process
|
||||
// This works with 5 ms on a T-Lora Pager, so we give it 10 ms to be safe
|
||||
TT_LOG_I(TAG, "Delay");
|
||||
kernel::delayMillis(10);
|
||||
|
||||
// TODO: Support for multiple displays
|
||||
TT_LOG_I(TAG, "Setup display");
|
||||
setupDisplay(); // Set backlight
|
||||
|
||||
// This event will likely block as other systems are initialized
|
||||
// e.g. Wi-Fi reads AP configs from SD card
|
||||
TT_LOG_I(TAG, "Publish event");
|
||||
kernel::publishSystemEvent(kernel::SystemEvent::BootSplash);
|
||||
|
||||
if (!setupUsbBootMode()) {
|
||||
TT_LOG_I(TAG, "initFromBootApp");
|
||||
initFromBootApp();
|
||||
waitForMinimalSplashDuration(start_time);
|
||||
service::loader::stopApp();
|
||||
@ -117,6 +133,17 @@ class BootApp : public App {
|
||||
|
||||
public:
|
||||
|
||||
void onCreate(AppContext& app) override {
|
||||
// Just in case this app is somehow resumed
|
||||
if (thread.getState() == Thread::State::Stopped) {
|
||||
thread.start();
|
||||
}
|
||||
}
|
||||
|
||||
void onDestroy(AppContext& app) override {
|
||||
thread.join();
|
||||
}
|
||||
|
||||
void onShow(TT_UNUSED AppContext& app, lv_obj_t* parent) override {
|
||||
auto* image = lv_image_create(parent);
|
||||
lv_obj_set_size(image, LV_PCT(100), LV_PCT(100));
|
||||
@ -128,15 +155,6 @@ public:
|
||||
lv_image_set_src(image, logo_path.c_str());
|
||||
|
||||
lvgl::obj_set_style_bg_blacken(parent);
|
||||
|
||||
// Just in case this app is somehow resumed
|
||||
if (thread.getState() == Thread::State::Stopped) {
|
||||
thread.start();
|
||||
}
|
||||
}
|
||||
|
||||
void onDestroy(AppContext& app) override {
|
||||
thread.join();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@ -166,8 +166,9 @@ class CalculatorApp : public App {
|
||||
void onShow(AppContext& context, lv_obj_t* parent) override {
|
||||
lv_obj_remove_flag(parent, LV_OBJ_FLAG_SCROLLABLE);
|
||||
lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN);
|
||||
lv_obj_set_style_pad_row(parent, 0, LV_STATE_DEFAULT);
|
||||
|
||||
lv_obj_t* toolbar = tt::lvgl::toolbar_create(parent, context);
|
||||
lv_obj_t* toolbar = lvgl::toolbar_create(parent, context);
|
||||
lv_obj_align(toolbar, LV_ALIGN_TOP_MID, 0, 0);
|
||||
|
||||
lv_obj_t* wrapper = lv_obj_create(parent);
|
||||
@ -208,10 +209,9 @@ class CalculatorApp : public App {
|
||||
lv_obj_set_style_pad_all(btnm, 5, LV_PART_MAIN);
|
||||
lv_obj_set_style_pad_row(btnm, 10, LV_PART_MAIN);
|
||||
lv_obj_set_style_pad_column(btnm, 5, LV_PART_MAIN);
|
||||
lv_obj_set_style_border_width(btnm, 0, LV_PART_MAIN);
|
||||
lv_obj_set_style_border_width(btnm, 2, LV_PART_MAIN);
|
||||
lv_obj_set_style_bg_color(btnm, lv_palette_main(LV_PALETTE_BLUE), LV_PART_ITEMS);
|
||||
|
||||
lv_obj_set_style_border_width(btnm, 0, LV_PART_MAIN);
|
||||
if (lv_display_get_horizontal_resolution(nullptr) <= 240 || lv_display_get_vertical_resolution(nullptr) <= 240) { //small screens
|
||||
lv_obj_set_size(btnm, lv_pct(100), lv_pct(60));
|
||||
} else { //large screens
|
||||
|
||||
@ -33,28 +33,15 @@ class ChatApp : public App {
|
||||
lv_obj_scroll_to_y(msg_list, lv_obj_get_scroll_y(msg_list) + 20, LV_ANIM_ON);
|
||||
}
|
||||
|
||||
static void onQuickSendClicked(lv_event_t* e) {
|
||||
auto* self = static_cast<ChatApp*>(lv_event_get_user_data(e));
|
||||
auto* btn = static_cast<lv_obj_t*>(lv_event_get_target(e));
|
||||
const char* message = lv_label_get_text(lv_obj_get_child(btn, 0));
|
||||
|
||||
if (message) {
|
||||
self->addMessage(message);
|
||||
if (!service::espnow::send(BROADCAST_ADDRESS, (const uint8_t*)message, strlen(message))) {
|
||||
TT_LOG_E(TAG, "Failed to send message");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void onSendClicked(lv_event_t* e) {
|
||||
auto* self = static_cast<ChatApp*>(lv_event_get_user_data(e));
|
||||
auto* msg = lv_textarea_get_text(self->input_field);
|
||||
auto msg_len = strlen(msg);
|
||||
const auto msg_len = strlen(msg);
|
||||
|
||||
if (self->msg_list && msg && msg_len) {
|
||||
self->addMessage(msg);
|
||||
|
||||
if (!service::espnow::send(BROADCAST_ADDRESS, (const uint8_t*)msg, msg_len)) {
|
||||
if (!service::espnow::send(BROADCAST_ADDRESS, reinterpret_cast<const uint8_t*>(msg), msg_len)) {
|
||||
TT_LOG_E(TAG, "Failed to send message");
|
||||
}
|
||||
|
||||
@ -64,14 +51,14 @@ class ChatApp : public App {
|
||||
|
||||
void onReceive(const esp_now_recv_info_t* receiveInfo, const uint8_t* data, int length) {
|
||||
// Append \0 to make it a string
|
||||
char* buffer = (char*)malloc(length + 1);
|
||||
auto buffer = static_cast<char*>(malloc(length + 1));
|
||||
memcpy(buffer, data, length);
|
||||
buffer[length] = 0x00;
|
||||
std::string message_prefixed = std::string("Received: ") + buffer;
|
||||
const std::string message_prefixed = std::string("Received: ") + buffer;
|
||||
|
||||
tt::lvgl::getSyncLock()->lock();
|
||||
lvgl::getSyncLock()->lock();
|
||||
addMessage(message_prefixed.c_str());
|
||||
tt::lvgl::getSyncLock()->unlock();
|
||||
lvgl::getSyncLock()->unlock();
|
||||
|
||||
free(buffer);
|
||||
}
|
||||
@ -82,7 +69,7 @@ public:
|
||||
// TODO: Move this to a configuration screen/app
|
||||
static const uint8_t key[ESP_NOW_KEY_LEN] = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };
|
||||
auto config = service::espnow::EspNowConfig(
|
||||
(uint8_t*)key,
|
||||
const_cast<uint8_t*>(key),
|
||||
service::espnow::Mode::Station,
|
||||
1,
|
||||
false,
|
||||
@ -105,58 +92,39 @@ public:
|
||||
}
|
||||
|
||||
void onShow(AppContext& context, 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);
|
||||
|
||||
// Create toolbar
|
||||
auto* toolbar = tt::lvgl::toolbar_create(parent, context);
|
||||
lv_obj_align(toolbar, LV_ALIGN_TOP_MID, 0, 0);
|
||||
const int toolbar_height = lv_obj_get_height(toolbar);
|
||||
lvgl::toolbar_create(parent, context);
|
||||
|
||||
// Message list
|
||||
msg_list = lv_list_create(parent);
|
||||
lv_obj_set_size(msg_list, lv_pct(75), lv_pct(43));
|
||||
lv_obj_align(msg_list, LV_ALIGN_TOP_LEFT, 5, toolbar_height + 45);
|
||||
lv_obj_set_flex_grow(msg_list, 1);
|
||||
lv_obj_set_width(msg_list, LV_PCT(100));
|
||||
lv_obj_set_flex_grow(msg_list, 1);
|
||||
lv_obj_set_style_bg_color(msg_list, lv_color_hex(0x262626), 0);
|
||||
lv_obj_set_style_border_width(msg_list, 1, 0);
|
||||
lv_obj_set_style_pad_all(msg_list, 5, 0);
|
||||
|
||||
// Quick message panel
|
||||
auto* quick_panel = lv_obj_create(parent);
|
||||
lv_obj_set_size(quick_panel, lv_pct(20), lv_pct(58));
|
||||
lv_obj_align(quick_panel, LV_ALIGN_TOP_RIGHT, -5, toolbar_height + 15); // Adjusted to match
|
||||
lv_obj_set_flex_flow(quick_panel, LV_FLEX_FLOW_COLUMN);
|
||||
lv_obj_set_flex_align(quick_panel, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
|
||||
lv_obj_set_style_pad_all(quick_panel, 5, 0);
|
||||
|
||||
// Quick message buttons
|
||||
const char* quick_msgs[] = {":-)", "+1", ":-("};
|
||||
for (size_t i = 0; i < sizeof(quick_msgs)/sizeof(quick_msgs[0]); i++) {
|
||||
lv_obj_t* btn = lv_btn_create(quick_panel);
|
||||
lv_obj_set_size(btn, lv_pct(75), 25);
|
||||
lv_obj_add_event_cb(btn, onQuickSendClicked, LV_EVENT_CLICKED, this);
|
||||
|
||||
lv_obj_t* label = lv_label_create(btn);
|
||||
lv_label_set_text(label, quick_msgs[i]);
|
||||
lv_obj_center(label);
|
||||
}
|
||||
lv_obj_set_style_pad_ver(msg_list, 0, 0);
|
||||
lv_obj_set_style_pad_hor(msg_list, 4, 0);
|
||||
|
||||
// Input panel
|
||||
auto* input_panel = lv_obj_create(parent);
|
||||
lv_obj_set_size(input_panel, lv_pct(95), 60);
|
||||
lv_obj_align(input_panel, LV_ALIGN_BOTTOM_MID, 0, -5);
|
||||
lv_obj_set_flex_flow(input_panel, LV_FLEX_FLOW_ROW);
|
||||
lv_obj_set_flex_align(input_panel, LV_FLEX_ALIGN_SPACE_BETWEEN, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
|
||||
lv_obj_set_style_pad_all(input_panel, 5, 0);
|
||||
auto* bottom_wrapper = lv_obj_create(parent);
|
||||
lv_obj_set_flex_flow(bottom_wrapper, LV_FLEX_FLOW_ROW);
|
||||
lv_obj_set_size(bottom_wrapper, LV_PCT(100), LV_SIZE_CONTENT);
|
||||
lv_obj_set_style_pad_all(bottom_wrapper, 0, 0);
|
||||
lv_obj_set_style_pad_column(bottom_wrapper, 4, 0);
|
||||
lv_obj_set_style_border_opa(bottom_wrapper, 0, LV_STATE_DEFAULT);
|
||||
|
||||
// Input field
|
||||
input_field = lv_textarea_create(input_panel);
|
||||
input_field = lv_textarea_create(bottom_wrapper);
|
||||
lv_obj_set_flex_grow(input_field, 1);
|
||||
lv_obj_set_height(input_field, LV_PCT(100));
|
||||
lv_textarea_set_placeholder_text(input_field, "Type a message...");
|
||||
lv_textarea_set_one_line(input_field, true);
|
||||
|
||||
// Send button
|
||||
auto* send_btn = lv_btn_create(input_panel);
|
||||
lv_obj_set_size(send_btn, 80, LV_PCT(100));
|
||||
auto* send_btn = lv_button_create(bottom_wrapper);
|
||||
lv_obj_set_style_margin_all(send_btn, 0, LV_STATE_DEFAULT);
|
||||
lv_obj_set_style_margin_top(send_btn, 2, LV_STATE_DEFAULT); // Hack to fix alignment
|
||||
lv_obj_add_event_cb(send_btn, onSendClicked, LV_EVENT_CLICKED, this);
|
||||
|
||||
auto* btn_label = lv_label_create(send_btn);
|
||||
|
||||
@ -91,6 +91,7 @@ public:
|
||||
|
||||
void onShow(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
|
||||
lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN);
|
||||
|
||||
@ -67,6 +67,7 @@ public:
|
||||
displaySettings = settings::display::loadOrGetDefault();
|
||||
|
||||
lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN);
|
||||
lv_obj_set_style_pad_row(parent, 0, LV_STATE_DEFAULT);
|
||||
|
||||
auto hal_display = getHalDisplay();
|
||||
assert(hal_display != nullptr);
|
||||
|
||||
@ -28,7 +28,7 @@ public:
|
||||
}
|
||||
|
||||
void onResult(AppContext& appContext, TT_UNUSED LaunchId launchId, Result result, std::unique_ptr<Bundle> bundle) override {
|
||||
view->onResult(result, std::move(bundle));
|
||||
view->onResult(launchId, result, std::move(bundle));
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@ -5,13 +5,8 @@ namespace tt::app::filebrowser {
|
||||
|
||||
constexpr auto* TAG = "FileBrowser";
|
||||
|
||||
bool isSupportedExecutableFile(const std::string& filename) {
|
||||
#ifdef ESP_PLATFORM
|
||||
// Currently only the PNG library is built into Tactility
|
||||
return filename.ends_with(".elf");
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
bool isSupportedAppFile(const std::string& filename) {
|
||||
return filename.ends_with(".app");
|
||||
}
|
||||
|
||||
bool isSupportedImageFile(const std::string& filename) {
|
||||
|
||||
@ -86,11 +86,12 @@ void View::viewFile(const std::string& path, const std::string& filename) {
|
||||
|
||||
TT_LOG_I(TAG, "Clicked %s", file_path.c_str());
|
||||
|
||||
if (isSupportedExecutableFile(filename)) {
|
||||
if (isSupportedAppFile(filename)) {
|
||||
#ifdef ESP_PLATFORM
|
||||
registerElfApp(processed_filepath);
|
||||
auto app_id = getElfAppId(processed_filepath);
|
||||
service::loader::startApp(app_id);
|
||||
// install(filename);
|
||||
auto message = std::format("Do you want to install {}?", filename);
|
||||
installAppPath = processed_filepath;
|
||||
installAppLaunchId = alertdialog::start("Install?", message, { "Yes", "No" });
|
||||
#endif
|
||||
} else if (isSupportedImageFile(filename)) {
|
||||
imageviewer::start(processed_filepath);
|
||||
@ -253,6 +254,7 @@ void View::update() {
|
||||
|
||||
void View::init(lv_obj_t* parent) {
|
||||
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, "Files");
|
||||
navigate_up_button = lvgl::toolbar_add_button_action(toolbar, LV_SYMBOL_UP, &onNavigateUpPressedCallback, this);
|
||||
@ -292,11 +294,20 @@ void View::onNavigate() {
|
||||
}
|
||||
}
|
||||
|
||||
void View::onResult(Result result, std::unique_ptr<Bundle> bundle) {
|
||||
void View::onResult(LaunchId launchId, Result result, std::unique_ptr<Bundle> bundle) {
|
||||
if (result != Result::Ok || bundle == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
launchId == installAppLaunchId &&
|
||||
result == Result::Ok &&
|
||||
alertdialog::getResultIndex(*bundle) == 0
|
||||
) {
|
||||
install(installAppPath);
|
||||
return;
|
||||
}
|
||||
|
||||
std::string filepath = state->getSelectedChildPath();
|
||||
TT_LOG_I(TAG, "Result for %s", filepath.c_str());
|
||||
|
||||
|
||||
@ -175,6 +175,7 @@ void View::update() {
|
||||
|
||||
void View::init(lv_obj_t* parent, Mode mode) {
|
||||
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, "Select File");
|
||||
navigate_up_button = lvgl::toolbar_add_button_action(toolbar, LV_SYMBOL_UP, &onNavigateUpPressedCallback, this);
|
||||
|
||||
@ -104,6 +104,7 @@ void GpioApp::stopTask() {
|
||||
|
||||
void GpioApp::onShow(AppContext& app, lv_obj_t* parent) {
|
||||
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);
|
||||
lv_obj_align(toolbar, LV_ALIGN_TOP_MID, 0, 0);
|
||||
|
||||
|
||||
@ -276,6 +276,7 @@ public:
|
||||
|
||||
void onShow(AppContext& app, lv_obj_t* parent) final {
|
||||
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);
|
||||
|
||||
|
||||
@ -95,6 +95,7 @@ int32_t I2cScannerApp::getLastBusIndex() {
|
||||
|
||||
void I2cScannerApp::onShow(AppContext& app, lv_obj_t* parent) {
|
||||
lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN);
|
||||
lv_obj_set_style_pad_row(parent, 0, LV_STATE_DEFAULT);
|
||||
|
||||
lvgl::toolbar_create(parent, app);
|
||||
|
||||
|
||||
@ -75,6 +75,7 @@ class I2cSettingsApp : public App {
|
||||
|
||||
void onShow(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);
|
||||
lvgl::toolbar_create(parent, app);
|
||||
|
||||
auto* wrapper = lv_obj_create(parent);
|
||||
|
||||
@ -85,6 +85,7 @@ public:
|
||||
textResources.load();
|
||||
|
||||
lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN);
|
||||
lv_obj_set_style_pad_row(parent, 0, LV_STATE_DEFAULT);
|
||||
|
||||
lvgl::toolbar_create(parent, app);
|
||||
|
||||
|
||||
@ -74,6 +74,8 @@ public:
|
||||
|
||||
void onShow(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);
|
||||
lvgl::toolbar_add_button_action(toolbar, LV_SYMBOL_EDIT, onLevelFilterPressedCallback, this);
|
||||
|
||||
|
||||
@ -123,6 +123,7 @@ class NotesApp : public App {
|
||||
void onShow(AppContext& context, lv_obj_t* parent) override {
|
||||
lv_obj_remove_flag(parent, LV_OBJ_FLAG_SCROLLABLE);
|
||||
lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN);
|
||||
lv_obj_set_style_pad_row(parent, 0, LV_STATE_DEFAULT);
|
||||
|
||||
lv_obj_t* toolbar = lvgl::toolbar_create(parent, context);
|
||||
lv_obj_align(toolbar, LV_ALIGN_TOP_MID, 0, 0);
|
||||
|
||||
@ -144,6 +144,7 @@ public:
|
||||
|
||||
void onShow(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);
|
||||
|
||||
lvgl::toolbar_create(parent, app);
|
||||
|
||||
|
||||
@ -255,6 +255,8 @@ void ScreenshotApp::onShow(AppContext& appContext, lv_obj_t* parent) {
|
||||
}
|
||||
|
||||
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, appContext);
|
||||
lv_obj_align(toolbar, LV_ALIGN_TOP_MID, 0, 0);
|
||||
|
||||
|
||||
@ -73,6 +73,8 @@ public:
|
||||
|
||||
void onShow(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);
|
||||
|
||||
std::string title = getTitleParameter(app.getParameters());
|
||||
lvgl::toolbar_create(parent, title);
|
||||
|
||||
|
||||
@ -61,6 +61,8 @@ public:
|
||||
|
||||
void onShow(AppContext& app, lv_obj_t* parent) final {
|
||||
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);
|
||||
|
||||
disconnectButton = lvgl::toolbar_add_button_action(toolbar, LV_SYMBOL_POWER, onDisconnectPressed, this);
|
||||
|
||||
@ -27,6 +27,7 @@ class SettingsApp : public App {
|
||||
|
||||
void onShow(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);
|
||||
|
||||
lvgl::toolbar_create(parent, app);
|
||||
|
||||
|
||||
@ -4,6 +4,7 @@
|
||||
#include <Tactility/hal/Device.h>
|
||||
#include <Tactility/Tactility.h>
|
||||
|
||||
#include <format>
|
||||
#include <lvgl.h>
|
||||
#include <utility>
|
||||
|
||||
@ -84,17 +85,17 @@ static std::string getStorageUnitString(StorageUnit unit) {
|
||||
}
|
||||
}
|
||||
|
||||
static uint64_t getStorageValue(StorageUnit unit, uint64_t bytes) {
|
||||
static std::string getStorageValue(StorageUnit unit, uint64_t bytes) {
|
||||
using enum StorageUnit;
|
||||
switch (unit) {
|
||||
case Bytes:
|
||||
return bytes;
|
||||
return std::to_string(bytes);
|
||||
case Kilobytes:
|
||||
return bytes / 1024;
|
||||
return std::to_string(bytes / 1024);
|
||||
case Megabytes:
|
||||
return bytes / 1024 / 1024;
|
||||
return std::format("{:.1f}", static_cast<float>(bytes) / 1024.f / 1024.f);
|
||||
case Gigabytes:
|
||||
return bytes / 1024 / 1024 / 1024;
|
||||
return std::format("{:.1f}", static_cast<float>(bytes) / 1024.f / 1024.f / 1024.f);
|
||||
default:
|
||||
std::unreachable();
|
||||
}
|
||||
@ -107,6 +108,7 @@ static void addMemoryBar(lv_obj_t* parent, const char* label, uint64_t free, uin
|
||||
lv_obj_set_style_pad_all(container, 0, 0);
|
||||
lv_obj_set_style_border_width(container, 0, 0);
|
||||
lv_obj_set_flex_flow(container, LV_FLEX_FLOW_ROW);
|
||||
lv_obj_set_style_bg_opa(container, 0, LV_STATE_DEFAULT);
|
||||
|
||||
auto* left_label = lv_label_create(container);
|
||||
lv_label_set_text(left_label, label);
|
||||
@ -115,22 +117,31 @@ static void addMemoryBar(lv_obj_t* parent, const char* label, uint64_t free, uin
|
||||
auto* bar = lv_bar_create(container);
|
||||
lv_obj_set_flex_grow(bar, 1);
|
||||
|
||||
// Scale down the uint64_t until it fits int32_t for the lv_bar
|
||||
uint64_t free_scaled = free;
|
||||
uint64_t total_scaled = total;
|
||||
while (total_scaled > static_cast<uint64_t>(INT32_MAX)) {
|
||||
free_scaled /= 1024;
|
||||
total_scaled /= 1024;
|
||||
}
|
||||
|
||||
if (total > 0) {
|
||||
lv_bar_set_range(bar, 0, (int32_t)total);
|
||||
lv_bar_set_range(bar, 0, total_scaled);
|
||||
} else {
|
||||
lv_bar_set_range(bar, 0, 1);
|
||||
}
|
||||
|
||||
lv_bar_set_value(bar, (int32_t)used, LV_ANIM_OFF);
|
||||
lv_bar_set_value(bar, (total_scaled - free_scaled), LV_ANIM_OFF);
|
||||
|
||||
auto* bottom_label = lv_label_create(parent);
|
||||
const auto unit = getStorageUnit(total);
|
||||
const auto unit_label = getStorageUnitString(unit);
|
||||
const auto used_converted = getStorageValue(unit, used);
|
||||
const auto total_converted = getStorageValue(unit, total);
|
||||
lv_label_set_text_fmt(bottom_label, "%llu / %llu %s", used_converted, total_converted, unit_label.c_str());
|
||||
lv_label_set_text_fmt(bottom_label, "%s / %s %s used", used_converted.c_str(), total_converted.c_str(), unit_label.c_str());
|
||||
lv_obj_set_width(bottom_label, LV_PCT(100));
|
||||
lv_obj_set_style_text_align(bottom_label, LV_TEXT_ALIGN_RIGHT, 0);
|
||||
lv_obj_set_style_pad_bottom(bottom_label, 12, LV_STATE_DEFAULT);
|
||||
}
|
||||
|
||||
#if configUSE_TRACE_FACILITY
|
||||
@ -164,6 +175,7 @@ static void addRtosTasks(lv_obj_t* parent) {
|
||||
auto* tasks = (TaskStatus_t*)malloc(sizeof(TaskStatus_t) * count);
|
||||
uint32_t totalRuntime = 0;
|
||||
UBaseType_t actual = uxTaskGetSystemState(tasks, count, &totalRuntime);
|
||||
|
||||
for (int i = 0; i < actual; ++i) {
|
||||
const TaskStatus_t& task = tasks[i];
|
||||
addRtosTask(parent, task);
|
||||
@ -189,6 +201,7 @@ class SystemInfoApp : public App {
|
||||
|
||||
void onShow(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);
|
||||
lvgl::toolbar_create(parent, app);
|
||||
|
||||
// This wrapper automatically has its children added vertically underneath eachother
|
||||
@ -197,43 +210,54 @@ class SystemInfoApp : public App {
|
||||
lv_obj_set_flex_flow(wrapper, LV_FLEX_FLOW_COLUMN);
|
||||
lv_obj_set_width(wrapper, LV_PCT(100));
|
||||
lv_obj_set_flex_grow(wrapper, 1);
|
||||
lv_obj_set_style_pad_all(wrapper, 0, LV_STATE_DEFAULT);
|
||||
|
||||
// Wrapper for the memory usage bars
|
||||
auto* memory_label = lv_label_create(wrapper);
|
||||
lv_label_set_text(memory_label, "Memory usage");
|
||||
auto* memory_wrapper = lv_obj_create(wrapper);
|
||||
lv_obj_set_flex_flow(memory_wrapper, LV_FLEX_FLOW_COLUMN);
|
||||
lv_obj_set_size(memory_wrapper, LV_PCT(100), LV_SIZE_CONTENT);
|
||||
auto* tabview = lv_tabview_create(wrapper);
|
||||
lv_tabview_set_tab_bar_position(tabview, LV_DIR_LEFT);
|
||||
lv_tabview_set_tab_bar_size(tabview, 80);
|
||||
|
||||
addMemoryBar(memory_wrapper, "Internal", getHeapFree(), getHeapTotal());
|
||||
// Tabs
|
||||
|
||||
auto* memory_tab = lv_tabview_add_tab(tabview, "Memory");
|
||||
lv_obj_set_flex_flow(memory_tab, LV_FLEX_FLOW_COLUMN);
|
||||
lv_obj_set_style_pad_row(memory_tab, 0, LV_STATE_DEFAULT);
|
||||
auto* storage_tab = lv_tabview_add_tab(tabview, "Storage");
|
||||
lv_obj_set_flex_flow(storage_tab, LV_FLEX_FLOW_COLUMN);
|
||||
lv_obj_set_style_pad_row(storage_tab, 0, LV_STATE_DEFAULT);
|
||||
auto* tasks_tab = lv_tabview_add_tab(tabview, "Tasks");
|
||||
lv_obj_set_flex_flow(tasks_tab, LV_FLEX_FLOW_COLUMN);
|
||||
lv_obj_set_style_pad_row(tasks_tab, 4, LV_STATE_DEFAULT);
|
||||
auto* devices_tab = lv_tabview_add_tab(tabview, "Devices");
|
||||
lv_obj_set_flex_flow(devices_tab, LV_FLEX_FLOW_COLUMN);
|
||||
lv_obj_set_style_pad_row(devices_tab, 4, LV_STATE_DEFAULT);
|
||||
auto* about_tab = lv_tabview_add_tab(tabview, "About");
|
||||
lv_obj_set_flex_flow(about_tab, LV_FLEX_FLOW_COLUMN);
|
||||
|
||||
// Memory tab content
|
||||
|
||||
addMemoryBar(memory_tab, "Internal", getHeapFree(), getHeapTotal());
|
||||
if (getSpiTotal() > 0) {
|
||||
addMemoryBar(memory_wrapper, "External", getSpiFree(), getSpiTotal());
|
||||
addMemoryBar(memory_tab, "External", getSpiFree(), getSpiTotal());
|
||||
}
|
||||
|
||||
#ifdef ESP_PLATFORM
|
||||
// Wrapper for the memory usage bars
|
||||
auto* storage_label = lv_label_create(wrapper);
|
||||
lv_label_set_text(storage_label, "Storage usage");
|
||||
auto* storage_wrapper = lv_obj_create(wrapper);
|
||||
lv_obj_set_flex_flow(storage_wrapper, LV_FLEX_FLOW_COLUMN);
|
||||
lv_obj_set_size(storage_wrapper, LV_PCT(100), LV_SIZE_CONTENT);
|
||||
|
||||
uint64_t storage_total = 0;
|
||||
uint64_t storage_free = 0;
|
||||
|
||||
if (esp_vfs_fat_info(file::MOUNT_POINT_SYSTEM, &storage_total, &storage_free) == ESP_OK) {
|
||||
addMemoryBar(storage_wrapper, file::MOUNT_POINT_SYSTEM, storage_free, storage_total);
|
||||
addMemoryBar(storage_tab, file::MOUNT_POINT_SYSTEM, storage_free, storage_total);
|
||||
}
|
||||
|
||||
if (esp_vfs_fat_info(file::MOUNT_POINT_DATA, &storage_total, &storage_free) == ESP_OK) {
|
||||
addMemoryBar(storage_wrapper, file::MOUNT_POINT_DATA, storage_free, storage_total);
|
||||
addMemoryBar(storage_tab, file::MOUNT_POINT_DATA, storage_free, storage_total);
|
||||
}
|
||||
|
||||
const auto sdcard_devices = hal::findDevices<hal::sdcard::SdCardDevice>(hal::Device::Type::SdCard);
|
||||
for (const auto& sdcard : sdcard_devices) {
|
||||
if (sdcard->isMounted() && esp_vfs_fat_info(sdcard->getMountPath().c_str(), &storage_total, &storage_free) == ESP_OK) {
|
||||
addMemoryBar(
|
||||
storage_wrapper,
|
||||
storage_tab,
|
||||
sdcard->getMountPath().c_str(),
|
||||
storage_free,
|
||||
storage_total
|
||||
@ -243,30 +267,17 @@ class SystemInfoApp : public App {
|
||||
#endif
|
||||
|
||||
#if configUSE_TRACE_FACILITY
|
||||
auto* tasks_label = lv_label_create(wrapper);
|
||||
lv_label_set_text(tasks_label, "Tasks");
|
||||
auto* tasks_wrapper = lv_obj_create(wrapper);
|
||||
lv_obj_set_flex_flow(tasks_wrapper, LV_FLEX_FLOW_COLUMN);
|
||||
lv_obj_set_size(tasks_wrapper, LV_PCT(100), LV_SIZE_CONTENT);
|
||||
addRtosTasks(tasks_wrapper);
|
||||
addRtosTasks(tasks_tab);
|
||||
#endif
|
||||
auto* devices_label = lv_label_create(wrapper);
|
||||
lv_label_set_text(devices_label, "Devices");
|
||||
auto* devices_wrapper = lv_obj_create(wrapper);
|
||||
lv_obj_set_flex_flow(devices_wrapper, LV_FLEX_FLOW_COLUMN);
|
||||
lv_obj_set_size(devices_wrapper, LV_PCT(100), LV_SIZE_CONTENT);
|
||||
addDevices(devices_wrapper);
|
||||
|
||||
#ifdef ESP_PLATFORM
|
||||
addDevices(devices_tab);
|
||||
|
||||
// Build info
|
||||
auto* build_info_label = lv_label_create(wrapper);
|
||||
lv_label_set_text(build_info_label, "Build info");
|
||||
auto* build_info_wrapper = lv_obj_create(wrapper);
|
||||
lv_obj_set_flex_flow(build_info_wrapper, LV_FLEX_FLOW_COLUMN);
|
||||
lv_obj_set_size(build_info_wrapper, LV_PCT(100), LV_SIZE_CONTENT);
|
||||
|
||||
auto* esp_idf_version = lv_label_create(build_info_wrapper);
|
||||
lv_label_set_text_fmt(esp_idf_version, "IDF version: %d.%d.%d", ESP_IDF_VERSION_MAJOR, ESP_IDF_VERSION_MINOR, ESP_IDF_VERSION_PATCH);
|
||||
auto* tactility_version = lv_label_create(about_tab);
|
||||
lv_label_set_text_fmt(tactility_version, "Tactility v%s", TT_VERSION);
|
||||
#ifdef ESP_PLATFORM
|
||||
auto* esp_idf_version = lv_label_create(about_tab);
|
||||
lv_label_set_text_fmt(esp_idf_version, "ESP-IDF v%d.%d.%d", ESP_IDF_VERSION_MAJOR, ESP_IDF_VERSION_MINOR, ESP_IDF_VERSION_PATCH);
|
||||
#endif
|
||||
}
|
||||
};
|
||||
|
||||
@ -26,6 +26,7 @@ public:
|
||||
|
||||
void onShow(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);
|
||||
|
||||
lvgl::toolbar_create(parent, app);
|
||||
|
||||
|
||||
@ -193,6 +193,8 @@ public:
|
||||
|
||||
void onShow(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);
|
||||
|
||||
lvgl::toolbar_create(parent, app);
|
||||
|
||||
auto* search_wrapper = lv_obj_create(parent);
|
||||
|
||||
@ -64,6 +64,8 @@ class WifiApSettings : public App {
|
||||
std::string ssid = paremeters->getString("ssid");
|
||||
|
||||
lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN);
|
||||
lv_obj_set_style_pad_row(parent, 0, LV_STATE_DEFAULT);
|
||||
|
||||
lvgl::toolbar_create(parent, ssid);
|
||||
|
||||
// Wrappers
|
||||
|
||||
@ -109,8 +109,9 @@ void View::createBottomButtons(lv_obj_t* parent) {
|
||||
|
||||
// TODO: Standardize dialogs
|
||||
void View::init(AppContext& app, lv_obj_t* parent) {
|
||||
|
||||
lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN);
|
||||
lv_obj_set_style_pad_row(parent, 0, LV_STATE_DEFAULT);
|
||||
|
||||
lvgl::toolbar_create(parent, app);
|
||||
|
||||
auto* wrapper = lv_obj_create(parent);
|
||||
|
||||
@ -272,12 +272,15 @@ void View::updateEnableOnBootToggle() {
|
||||
// region Main
|
||||
|
||||
void View::init(const AppContext& app, lv_obj_t* parent) {
|
||||
lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN);
|
||||
lv_obj_set_style_pad_row(parent, 0, LV_STATE_DEFAULT);
|
||||
|
||||
root = parent;
|
||||
|
||||
paths = app.getPaths();
|
||||
|
||||
// Toolbar
|
||||
lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN);
|
||||
|
||||
lv_obj_t* toolbar = lvgl::toolbar_create(parent, app);
|
||||
|
||||
scanning_spinner = lvgl::toolbar_add_spinner_action(toolbar);
|
||||
|
||||
@ -22,17 +22,22 @@ bool loadPropertiesFile(const std::string& filePath, std::function<void(const st
|
||||
return file::withLock<bool>(filePath, [&filePath, &callback] {
|
||||
TT_LOG_I(TAG, "Reading properties file %s", filePath.c_str());
|
||||
uint16_t line_count = 0;
|
||||
return readLines(filePath, true, [&line_count, &filePath, &callback](const std::string& line) {
|
||||
std::string key_prefix = "";
|
||||
return readLines(filePath, true, [&key_prefix, &line_count, &filePath, &callback](const std::string& line) {
|
||||
line_count++;
|
||||
std::string key, value;
|
||||
auto trimmed_line = string::trim(line, " \t");
|
||||
if (!trimmed_line.starts_with("#")) {
|
||||
if (getKeyValuePair(trimmed_line, key, value)) {
|
||||
std::string trimmed_key = string::trim(key, " \t");
|
||||
std::string trimmed_value = string::trim(value, " \t");
|
||||
callback(trimmed_key, trimmed_value);
|
||||
if (trimmed_line.starts_with("[")) {
|
||||
key_prefix = trimmed_line;
|
||||
} else {
|
||||
TT_LOG_E(TAG, "Failed to parse line %d of %s", line_count, filePath.c_str());
|
||||
if (getKeyValuePair(trimmed_line, key, value)) {
|
||||
std::string trimmed_key = key_prefix + string::trim(key, " \t");
|
||||
std::string trimmed_value = string::trim(value, " \t");
|
||||
callback(trimmed_key, trimmed_value);
|
||||
} else {
|
||||
TT_LOG_E(TAG, "Failed to parse line %d of %s", line_count, filePath.c_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@ -77,8 +77,15 @@ bool startService(const std::string& id) {
|
||||
instance_mutex.unlock();
|
||||
|
||||
service_instance->setState(State::Starting);
|
||||
service_instance->getService()->onStart(*service_instance);
|
||||
service_instance->setState(State::Started);
|
||||
if (service_instance->getService()->onStart(*service_instance)) {
|
||||
service_instance->setState(State::Started);
|
||||
} else {
|
||||
TT_LOG_E(TAG, "Starting %s failed", id.c_str());
|
||||
service_instance->setState(State::Stopped);
|
||||
instance_mutex.lock();
|
||||
service_instance_map.erase(manifest->id);
|
||||
instance_mutex.unlock();
|
||||
}
|
||||
|
||||
TT_LOG_I(TAG, "Started %s", id.c_str());
|
||||
|
||||
|
||||
@ -18,6 +18,8 @@
|
||||
#include <esp_wifi.h>
|
||||
#include <ranges>
|
||||
#include <sstream>
|
||||
#include <Tactility/Tactility.h>
|
||||
#include <Tactility/file/FileLock.h>
|
||||
|
||||
namespace tt::service::development {
|
||||
|
||||
@ -25,7 +27,7 @@ extern const ServiceManifest manifest;
|
||||
|
||||
constexpr const char* TAG = "DevService";
|
||||
|
||||
void DevelopmentService::onStart(ServiceContext& service) {
|
||||
bool DevelopmentService::onStart(ServiceContext& service) {
|
||||
auto lock = mutex.asScopedLock();
|
||||
lock.lock();
|
||||
|
||||
@ -39,6 +41,8 @@ void DevelopmentService::onStart(ServiceContext& service) {
|
||||
);
|
||||
|
||||
setEnabled(shouldEnableOnBoot());
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void DevelopmentService::onStop(ServiceContext& service) {
|
||||
@ -97,6 +101,7 @@ void DevelopmentService::startServer() {
|
||||
deviceResponse = stream.str();
|
||||
|
||||
httpd_config_t config = HTTPD_DEFAULT_CONFIG();
|
||||
config.stack_size = 5120;
|
||||
|
||||
config.server_port = 6666;
|
||||
config.uri_match_fn = httpd_uri_match_wildcard;
|
||||
@ -154,6 +159,8 @@ void DevelopmentService::onNetworkDisconnected() {
|
||||
// region endpoints
|
||||
|
||||
esp_err_t DevelopmentService::handleGetInfo(httpd_req_t* request) {
|
||||
TT_LOG_I(TAG, "GET /device");
|
||||
|
||||
if (httpd_resp_set_type(request, "application/json") != ESP_OK) {
|
||||
TT_LOG_W(TAG, "Failed to send header");
|
||||
return ESP_FAIL;
|
||||
@ -171,6 +178,8 @@ esp_err_t DevelopmentService::handleGetInfo(httpd_req_t* request) {
|
||||
}
|
||||
|
||||
esp_err_t DevelopmentService::handleAppRun(httpd_req_t* request) {
|
||||
TT_LOG_I(TAG, "POST /app/run");
|
||||
|
||||
std::string query;
|
||||
if (!network::getQueryOrSendError(request, query)) {
|
||||
return ESP_FAIL;
|
||||
@ -184,28 +193,15 @@ esp_err_t DevelopmentService::handleAppRun(httpd_req_t* request) {
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
auto app_id = id_key_pos->second;
|
||||
if (app_id.ends_with(".app.elf")) {
|
||||
if (!file::isFile(app_id)) {
|
||||
TT_LOG_W(TAG, "[400] /app/run cannot find app %s", app_id.c_str());
|
||||
httpd_resp_send_err(request, HTTPD_400_BAD_REQUEST, "app not found");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
app::registerElfApp(app_id);
|
||||
app_id = app::getElfAppId(app_id);
|
||||
} else if (!app::findAppById(app_id.c_str())) {
|
||||
TT_LOG_W(TAG, "[400] /app/run app not found");
|
||||
httpd_resp_send_err(request, HTTPD_400_BAD_REQUEST, "app not found");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
app::start(app_id);
|
||||
TT_LOG_I(TAG, "[200] /app/run %s", app_id.c_str());
|
||||
app::start(id_key_pos->second);
|
||||
TT_LOG_I(TAG, "[200] /app/run %s", id_key_pos->second.c_str());
|
||||
httpd_resp_send(request, nullptr, 0);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t DevelopmentService::handleAppInstall(httpd_req_t* request) {
|
||||
TT_LOG_I(TAG, "PUT /app/install");
|
||||
|
||||
std::string boundary;
|
||||
if (!network::getMultiPartBoundaryOrSendError(request, boundary)) {
|
||||
return false;
|
||||
@ -250,8 +246,19 @@ esp_err_t DevelopmentService::handleAppInstall(httpd_req_t* request) {
|
||||
}
|
||||
content_left -= content_read;
|
||||
|
||||
const std::string tmp_path = app::getTempPath();
|
||||
auto lock = file::getLock(tmp_path)->asScopedLock();
|
||||
|
||||
lock.lock();
|
||||
if (!file::findOrCreateDirectory(tmp_path, 0777)) {
|
||||
httpd_resp_send_err(request, HTTPD_500_INTERNAL_SERVER_ERROR, "Failed to save file");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
lock.unlock();
|
||||
|
||||
// Write file
|
||||
auto file_path = std::format("/data/{}", filename_entry->second);
|
||||
lock.lock();
|
||||
auto file_path = std::format("{}/{}", tmp_path, filename_entry->second);
|
||||
auto* file = fopen(file_path.c_str(), "wb");
|
||||
auto file_bytes_written = fwrite(buffer.get(), 1, file_size, file);
|
||||
fclose(file);
|
||||
@ -259,6 +266,8 @@ esp_err_t DevelopmentService::handleAppInstall(httpd_req_t* request) {
|
||||
httpd_resp_send_err(request, HTTPD_500_INTERNAL_SERVER_ERROR, "Failed to save file");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
lock.unlock();
|
||||
|
||||
|
||||
// Read and verify part
|
||||
if (!network::readAndDiscardOrSendError(request, part_after_file)) {
|
||||
@ -270,9 +279,21 @@ esp_err_t DevelopmentService::handleAppInstall(httpd_req_t* request) {
|
||||
TT_LOG_W(TAG, "We have more bytes at the end of the request parsing?!");
|
||||
}
|
||||
|
||||
if (!app::install(file_path)) {
|
||||
httpd_resp_send_err(request, HTTPD_500_INTERNAL_SERVER_ERROR, "Failed to install");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
lock.lock();
|
||||
if (remove(file_path.c_str()) != 0) {
|
||||
TT_LOG_W(TAG, "Failed to delete %s", file_path.c_str());
|
||||
}
|
||||
lock.unlock();
|
||||
|
||||
TT_LOG_I(TAG, "[200] /app/install -> %s", file_path.c_str());
|
||||
|
||||
httpd_resp_send(request, nullptr, 0);
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
|
||||
@ -20,11 +20,13 @@ static uint8_t BROADCAST_MAC[ESP_NOW_ETH_ALEN];
|
||||
|
||||
constexpr bool isBroadcastAddress(uint8_t address[ESP_NOW_ETH_ALEN]) { return memcmp(address, BROADCAST_MAC, ESP_NOW_ETH_ALEN) == 0; }
|
||||
|
||||
void EspNowService::onStart(ServiceContext& service) {
|
||||
bool EspNowService::onStart(ServiceContext& service) {
|
||||
auto lock = mutex.asScopedLock();
|
||||
lock.lock();
|
||||
|
||||
memset(BROADCAST_MAC, 0xFF, sizeof(BROADCAST_MAC));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void EspNowService::onStop(ServiceContext& service) {
|
||||
|
||||
@ -12,8 +12,8 @@ namespace tt::service::gps {
|
||||
constexpr const char* TAG = "GpsService";
|
||||
extern const ServiceManifest manifest;
|
||||
|
||||
constexpr inline bool hasTimeElapsed(TickType_t now, TickType_t timeInThePast, TickType_t expireTimeInTicks) {
|
||||
return (TickType_t)(now - timeInThePast) >= expireTimeInTicks;
|
||||
constexpr bool hasTimeElapsed(TickType_t now, TickType_t timeInThePast, TickType_t expireTimeInTicks) {
|
||||
return (now - timeInThePast) >= expireTimeInTicks;
|
||||
}
|
||||
|
||||
GpsService::GpsDeviceRecord* _Nullable GpsService::findGpsRecord(const std::shared_ptr<GpsDevice>& device) {
|
||||
@ -58,14 +58,14 @@ void GpsService::removeGpsDevice(const std::shared_ptr<GpsDevice>& device) {
|
||||
});
|
||||
}
|
||||
|
||||
void GpsService::onStart(tt::service::ServiceContext& serviceContext) {
|
||||
bool GpsService::onStart(ServiceContext& serviceContext) {
|
||||
auto lock = mutex.asScopedLock();
|
||||
lock.lock();
|
||||
|
||||
paths = serviceContext.getPaths();
|
||||
return true;
|
||||
}
|
||||
|
||||
void GpsService::onStop(tt::service::ServiceContext& serviceContext) {
|
||||
void GpsService::onStop(ServiceContext& serviceContext) {
|
||||
if (getState() == State::On) {
|
||||
stopReceiving();
|
||||
}
|
||||
|
||||
@ -112,7 +112,13 @@ void GuiService::redraw() {
|
||||
unlock();
|
||||
}
|
||||
|
||||
void GuiService::onStart(TT_UNUSED ServiceContext& service) {
|
||||
bool GuiService::onStart(TT_UNUSED ServiceContext& service) {
|
||||
auto* screen_root = lv_screen_active();
|
||||
if (screen_root == nullptr) {
|
||||
TT_LOG_E(TAG, "No display found");
|
||||
return false;
|
||||
}
|
||||
|
||||
thread = new Thread(
|
||||
"gui",
|
||||
4096, // Last known minimum was 2800 for launching desktop
|
||||
@ -123,12 +129,9 @@ void GuiService::onStart(TT_UNUSED ServiceContext& service) {
|
||||
onLoaderEvent(event);
|
||||
});
|
||||
|
||||
lvgl::lock(portMAX_DELAY);
|
||||
|
||||
tt_check(lvgl::lock(1000 / portTICK_PERIOD_MS));
|
||||
keyboardGroup = lv_group_create();
|
||||
auto* screen_root = lv_screen_active();
|
||||
assert(screen_root != nullptr);
|
||||
|
||||
lvgl::obj_set_style_bg_blacken(screen_root);
|
||||
|
||||
lv_obj_t* vertical_container = lv_obj_create(screen_root);
|
||||
@ -156,6 +159,8 @@ void GuiService::onStart(TT_UNUSED ServiceContext& service) {
|
||||
|
||||
thread->setPriority(THREAD_PRIORITY_SERVICE);
|
||||
thread->start();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void GuiService::onStop(TT_UNUSED ServiceContext& service) {
|
||||
|
||||
@ -62,8 +62,9 @@ class LoaderService final : public Service {
|
||||
|
||||
public:
|
||||
|
||||
void onStart(TT_UNUSED ServiceContext& service) override {
|
||||
bool onStart(TT_UNUSED ServiceContext& service) override {
|
||||
dispatcherThread->start();
|
||||
return true;
|
||||
}
|
||||
|
||||
void onStop(TT_UNUSED ServiceContext& service) override {
|
||||
|
||||
@ -2,16 +2,15 @@
|
||||
|
||||
#if TT_FEATURE_SCREENSHOT_ENABLED
|
||||
|
||||
#include "Tactility/service/screenshot/Screenshot.h"
|
||||
|
||||
#include <Tactility/service/ServiceContext.h>
|
||||
#include <Tactility/service/screenshot/Screenshot.h>
|
||||
#include <Tactility/service/ServiceRegistration.h>
|
||||
|
||||
#include <memory>
|
||||
#include <lvgl.h>
|
||||
|
||||
namespace tt::service::screenshot {
|
||||
|
||||
#define TAG "screenshot_service"
|
||||
constexpr auto* TAG = "ScreenshotService";
|
||||
|
||||
extern const ServiceManifest manifest;
|
||||
|
||||
@ -51,6 +50,15 @@ void ScreenshotService::startTimed(const std::string& path, uint8_t delayInSecon
|
||||
}
|
||||
}
|
||||
|
||||
bool ScreenshotService::onStart(ServiceContext& serviceContext) {
|
||||
if (lv_screen_active() == nullptr) {
|
||||
TT_LOG_E(TAG, "No display found");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void ScreenshotService::stop() {
|
||||
auto lock = mutex.asScopedLock();
|
||||
if (!lock.lock(50 / portTICK_PERIOD_MS)) {
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
#include <Tactility/service/ServiceRegistration.h>
|
||||
|
||||
#include <Tactility/Mutex.h>
|
||||
#include <Tactility/Tactility.h>
|
||||
#include <Tactility/Timer.h>
|
||||
#include <Tactility/hal/sdcard/SdCardDevice.h>
|
||||
|
||||
@ -52,13 +53,21 @@ class SdCardService final : public Service {
|
||||
|
||||
public:
|
||||
|
||||
void onStart(ServiceContext& serviceContext) override {
|
||||
bool onStart(ServiceContext& serviceContext) override {
|
||||
if (hal::findFirstDevice<hal::sdcard::SdCardDevice>(hal::Device::Type::SdCard) == nullptr) {
|
||||
TT_LOG_W(TAG, "No SD card device found - not starting Service");
|
||||
return false;
|
||||
}
|
||||
|
||||
auto service = findServiceById<SdCardService>(manifest.id);
|
||||
updateTimer = std::make_unique<Timer>(Timer::Type::Periodic, [service]() {
|
||||
service->update();
|
||||
});
|
||||
|
||||
// We want to try and scan more often in case of startup or scan lock failure
|
||||
updateTimer->start(1000);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void onStop(ServiceContext& serviceContext) override {
|
||||
|
||||
@ -249,7 +249,12 @@ public:
|
||||
lvgl::statusbar_icon_remove(gps_icon_id);
|
||||
}
|
||||
|
||||
void onStart(ServiceContext& serviceContext) override {
|
||||
bool onStart(ServiceContext& serviceContext) override {
|
||||
if (lv_screen_active() == nullptr) {
|
||||
TT_LOG_E(TAG, "No display found");
|
||||
return false;
|
||||
}
|
||||
|
||||
paths = serviceContext.getPaths();
|
||||
|
||||
// TODO: Make thread-safe for LVGL
|
||||
@ -265,6 +270,8 @@ public:
|
||||
|
||||
// We want to try and scan more often in case of startup or scan lock failure
|
||||
updateTimer->start(1000);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void onStop(ServiceContext& service) override{
|
||||
|
||||
@ -414,10 +414,9 @@ static bool copy_scan_list(std::shared_ptr<Wifi> wifi) {
|
||||
}
|
||||
|
||||
static bool find_auto_connect_ap(std::shared_ptr<Wifi> wifi, settings::WifiApSettings& settings) {
|
||||
TT_LOG_I(TAG, "find_auto_connect_ap()");
|
||||
auto lock = wifi->dataMutex.asScopedLock();
|
||||
|
||||
if (lock.lock(10 / portTICK_PERIOD_MS)) {
|
||||
TT_LOG_I(TAG, "auto_connect()");
|
||||
for (int i = 0; i < wifi->scan_list_count; ++i) {
|
||||
auto ssid = reinterpret_cast<const char*>(wifi->scan_list[i].ssid);
|
||||
if (settings::contains(ssid)) {
|
||||
@ -897,7 +896,7 @@ class WifiService final : public Service {
|
||||
|
||||
public:
|
||||
|
||||
void onStart(ServiceContext& service) override {
|
||||
bool onStart(ServiceContext& service) override {
|
||||
assert(wifi_singleton == nullptr);
|
||||
wifi_singleton = std::make_shared<Wifi>();
|
||||
|
||||
@ -913,6 +912,8 @@ public:
|
||||
TT_LOG_I(TAG, "Auto-enabling due to setting");
|
||||
getMainDispatcher().dispatch([] { dispatchEnable(wifi_singleton); });
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void onStop(ServiceContext& service) override {
|
||||
|
||||
@ -144,9 +144,10 @@ class WifiService final : public Service {
|
||||
|
||||
public:
|
||||
|
||||
void onStart(TT_UNUSED ServiceContext& service) final {
|
||||
bool onStart(TT_UNUSED ServiceContext& service) final {
|
||||
tt_check(wifi == nullptr);
|
||||
wifi = new Wifi();
|
||||
return true;
|
||||
}
|
||||
|
||||
void onStop(TT_UNUSED ServiceContext& service) final {
|
||||
|
||||
@ -67,7 +67,11 @@ bool writeString(const std::string& filepath, const std::string& content);
|
||||
* @param[in] mode the mode to use when creating directories
|
||||
* @return true when the specified path was found, or otherwise creates the directories recursively with the specified mode.
|
||||
*/
|
||||
bool findOrCreateDirectory(std::string path, mode_t mode);
|
||||
bool findOrCreateDirectory(const std::string& path, mode_t mode);
|
||||
|
||||
bool findOrCreateParentDirectory(const std::string& path, mode_t mode);
|
||||
|
||||
bool deleteRecursively(const std::string& path);
|
||||
|
||||
/**
|
||||
* Concatenate a child path with a parent path, ensuring proper slash inbetween
|
||||
@ -77,6 +81,8 @@ bool findOrCreateDirectory(std::string path, mode_t mode);
|
||||
*/
|
||||
std::string getChildPath(const std::string& basePath, const std::string& childPath);
|
||||
|
||||
std::string getLastPathSegment(const std::string& path);
|
||||
|
||||
typedef int (*ScandirFilter)(const dirent*);
|
||||
|
||||
typedef bool (*ScandirSort)(const dirent&, const dirent&);
|
||||
@ -91,6 +97,21 @@ bool isFile(const std::string& path);
|
||||
|
||||
bool isDirectory(const std::string& path);
|
||||
|
||||
/**
|
||||
* A scandir()-like implementation that works on ESP32.
|
||||
* It does not return "." and ".." items but otherwise functions the same.
|
||||
* It returns an allocated output array with allocated dirent instances.
|
||||
* The caller is responsible for free-ing the memory of these.
|
||||
*
|
||||
* @param[in] path path the scan for files and directories
|
||||
* @param[out] onEntry a pointer to a function that accepts an entry
|
||||
* @return true if the directory exists and listing its contents was successful
|
||||
*/
|
||||
bool listDirectory(
|
||||
const std::string& path,
|
||||
std::function<void(const dirent&)> onEntry
|
||||
);
|
||||
|
||||
/**
|
||||
* A scandir()-like implementation that works on ESP32.
|
||||
* It does not return "." and ".." items but otherwise functions the same.
|
||||
@ -106,8 +127,8 @@ bool isDirectory(const std::string& path);
|
||||
int scandir(
|
||||
const std::string& path,
|
||||
std::vector<dirent>& outList,
|
||||
ScandirFilter _Nullable filter,
|
||||
ScandirSort _Nullable sort
|
||||
ScandirFilter _Nullable filter = nullptr,
|
||||
ScandirSort _Nullable sort = nullptr
|
||||
);
|
||||
|
||||
bool readLines(const std::string& filePath, bool stripNewLine, std::function<void(const char* line)> callback);
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
#include <cstring>
|
||||
#include <fstream>
|
||||
#include <unistd.h>
|
||||
#include <Tactility/StringUtils.h>
|
||||
|
||||
namespace tt::hal::sdcard {
|
||||
class SdCardDevice;
|
||||
@ -35,6 +36,28 @@ bool direntSortAlphaAndType(const dirent& left, const dirent& right) {
|
||||
}
|
||||
}
|
||||
|
||||
bool listDirectory(
|
||||
const std::string& path,
|
||||
std::function<void(const dirent&)> onEntry
|
||||
) {
|
||||
TT_LOG_I(TAG, "listDir start %s", path.c_str());
|
||||
DIR* dir = opendir(path.c_str());
|
||||
if (dir == nullptr) {
|
||||
TT_LOG_E(TAG, "Failed to open dir %s", path.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
dirent* current_entry;
|
||||
while ((current_entry = readdir(dir)) != nullptr) {
|
||||
onEntry(*current_entry);
|
||||
}
|
||||
|
||||
closedir(dir);
|
||||
|
||||
TT_LOG_I(TAG, "listDir stop %s", path.c_str());
|
||||
return true;
|
||||
}
|
||||
|
||||
int scandir(
|
||||
const std::string& path,
|
||||
std::vector<dirent>& outList,
|
||||
@ -50,7 +73,7 @@ int scandir(
|
||||
|
||||
dirent* current_entry;
|
||||
while ((current_entry = readdir(dir)) != nullptr) {
|
||||
if (filterMethod(current_entry) == 0) {
|
||||
if (filterMethod == nullptr || filterMethod(current_entry) == 0) {
|
||||
outList.push_back(*current_entry);
|
||||
}
|
||||
}
|
||||
@ -184,7 +207,16 @@ static bool findOrCreateDirectoryInternal(std::string path, mode_t mode) {
|
||||
return true;
|
||||
}
|
||||
|
||||
bool findOrCreateDirectory(std::string path, mode_t mode) {
|
||||
std::string getLastPathSegment(const std::string& path) {
|
||||
auto index = path.find_last_of('/');
|
||||
if (index != std::string::npos) {
|
||||
return path.substr(index + 1);
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
bool findOrCreateDirectory(const std::string& path, mode_t mode) {
|
||||
if (path.empty()) {
|
||||
return true;
|
||||
}
|
||||
@ -213,6 +245,45 @@ bool findOrCreateDirectory(std::string path, mode_t mode) {
|
||||
return true;
|
||||
}
|
||||
|
||||
bool findOrCreateParentDirectory(const std::string& path, mode_t mode) {
|
||||
std::string parent;
|
||||
if (!string::getPathParent(path, parent)) {
|
||||
return false;
|
||||
}
|
||||
return findOrCreateDirectory(parent, mode);
|
||||
}
|
||||
|
||||
bool deleteRecursively(const std::string& path) {
|
||||
if (path.empty()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (isDirectory(path)) {
|
||||
std::vector<dirent> entries;
|
||||
if (scandir(path, entries) < 0) {
|
||||
TT_LOG_E(TAG, "Failed to scan directory %s", path.c_str());
|
||||
return false;
|
||||
}
|
||||
for (const auto& entry : entries) {
|
||||
auto child_path = path + "/" + entry.d_name;
|
||||
if (!deleteRecursively(child_path)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
TT_LOG_I(TAG, "Deleting %s", path.c_str());
|
||||
return rmdir(path.c_str()) == 0;
|
||||
} else if (isFile(path)) {
|
||||
TT_LOG_I(TAG, "Deleting %s", path.c_str());
|
||||
return remove(path.c_str()) == 0;
|
||||
} else if (path == "/" || path == "." || path == "..") {
|
||||
// No-op
|
||||
return true;
|
||||
} else {
|
||||
TT_LOG_E(TAG, "Failed to delete \"%s\": unknown type", path.c_str());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool isFile(const std::string& path) {
|
||||
return access(path.c_str(), F_OK) == 0;
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user