Implement posix app loading (#643)
- Added POSIX desktop support for SDK builds, application packaging, and integration testing. - Added POSIX filesystem partitions and improved application path handling. - Added simulator support for loading and running applications dynamically. - Improved simulator task stack handling and display startup reliability. - Improved simulator display scaling, resizing, high-DPI support, and pointer accuracy. - Fixed LVGL timers and input polling on POSIX. (fixes simulator with Linux on some Intel graphics platforms) - Standardized data paths across platforms. - Logging now works via separate task: this allows apps to write to log without it affecting their stdout (for apps that output text as relevant date for other apps, like the File Selection app) - Fixes for app stdio
This commit is contained in:
committed by
GitHub
parent
d3656bcd3d
commit
643cbc3806
@@ -0,0 +1,43 @@
|
||||
name: Build
|
||||
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
submodules: recursive
|
||||
persist-credentials: false
|
||||
- name: 'Detect architecture'
|
||||
id: arch
|
||||
shell: bash
|
||||
run: echo "value=$(uname -m)" >> "$GITHUB_OUTPUT"
|
||||
- name: 'Configure'
|
||||
shell: bash
|
||||
run: cmake -S ./ -B buildsim
|
||||
- name: 'Build'
|
||||
shell: bash
|
||||
run: cmake --build buildsim --target TactilityKernel lvgl minitar minmea $(cat Buildscripts/release-sdk-modules.txt)
|
||||
- name: 'Release'
|
||||
shell: bash
|
||||
run: python Buildscripts/release-sdk-posix.py release/TactilitySDK
|
||||
- name: 'Test Integration Prep'
|
||||
shell: bash
|
||||
# The manifest.properties of our integration test uses version 0.0.0 to indicate that it is not using a normal SDK
|
||||
# This way, it only works with our custom build. That means we have to create a copy of the SDK with the correct folder structure:
|
||||
env:
|
||||
TACTILITY_ARCH: ${{ steps.arch.outputs.value }}
|
||||
run: |
|
||||
TACTILITY_SDK_NAME="0.0.0-posix-$TACTILITY_ARCH"
|
||||
mkdir -p test_sdk/$TACTILITY_SDK_NAME
|
||||
cp -r release/TactilitySDK test_sdk/$TACTILITY_SDK_NAME
|
||||
- name: 'Test Integration'
|
||||
shell: bash
|
||||
env:
|
||||
TACTILITY_ARCH: ${{ steps.arch.outputs.value }}
|
||||
run: cd Tests/SdkIntegration && TACTILITY_SDK_PATH=../../test_sdk python tactility.py build posix-$TACTILITY_ARCH --local-sdk
|
||||
- name: 'Upload Artifact'
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: TactilitySDK-posix-${{ steps.arch.outputs.value }}
|
||||
path: release/TactilitySDK
|
||||
retention-days: 30
|
||||
@@ -29,7 +29,7 @@ runs:
|
||||
env:
|
||||
# NOTE: Update with ESP-IDF!
|
||||
ESP_IDF_VERSION: '5.5.2'
|
||||
run: python Buildscripts/release-sdk.py release/TactilitySDK
|
||||
run: python Buildscripts/release-sdk-esp32.py release/TactilitySDK
|
||||
- name: 'Test Integration Prep'
|
||||
shell: bash
|
||||
# The manifest.properties of our integration test uses version 0.0.0 to indicate that it is not using a normal SDK
|
||||
|
||||
@@ -11,7 +11,7 @@ on:
|
||||
permissions: read-all
|
||||
|
||||
jobs:
|
||||
BuildSdk:
|
||||
BuildSdkEsp32:
|
||||
strategy:
|
||||
matrix:
|
||||
board: [
|
||||
@@ -30,9 +30,17 @@ jobs:
|
||||
with:
|
||||
board_id: ${{ matrix.board.id }}
|
||||
arch: ${{ matrix.board.arch }}
|
||||
BuildSdkPosix:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: "Build SDK"
|
||||
uses: ./.github/actions/build-sdk-posix
|
||||
GenerateDeviceMatrix:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [ BuildSdk ]
|
||||
needs: [ BuildSdkEsp32 ]
|
||||
outputs:
|
||||
matrix: ${{ steps.set-matrix.outputs.matrix }}
|
||||
steps:
|
||||
@@ -57,7 +65,7 @@ jobs:
|
||||
arch: ${{ matrix.board.arch }}
|
||||
BundleArtifacts:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [ BuildFirmware ]
|
||||
needs: [ BuildFirmware, BuildSdkPosix ]
|
||||
if: |
|
||||
(github.event_name == 'push' && github.ref == 'refs/heads/main') ||
|
||||
(github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v'))
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
.DS_Store
|
||||
|
||||
build*/
|
||||
!.github/actions/build*/
|
||||
cmake*/
|
||||
CMakeCache.txt
|
||||
*.cbp
|
||||
@@ -25,3 +26,5 @@ sdkconfig.board.*.dev
|
||||
|
||||
.caveman.json
|
||||
.ai/mcp
|
||||
|
||||
__pycache__
|
||||
+16
-1
@@ -4,7 +4,12 @@ endfunction()
|
||||
function(_tactility_project)
|
||||
endfunction()
|
||||
|
||||
macro(tactility_project project_name)
|
||||
macro(tactility_project_pre project_name)
|
||||
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
|
||||
set(EXTRA_COMPONENT_DIRS ${TACTILITY_SDK_PATH} ${TACTILITY_SDK_PATH}/Modules)
|
||||
endmacro()
|
||||
|
||||
macro(tactility_project_post project_name)
|
||||
set(TACTILITY_SKIP_SPIFFS 1)
|
||||
|
||||
# Tactility's PanicHandler.cpp needs s0 to stay a frame pointer to capture a callstack for
|
||||
@@ -45,3 +50,13 @@ macro(tactility_project project_name)
|
||||
)
|
||||
|
||||
endmacro()
|
||||
|
||||
macro(tactility_component_register)
|
||||
cmake_parse_arguments(TT_COMPONENT "" "" "SRCS;INCLUDE_DIRS;REQUIRES;PRIV_REQUIRES" ${ARGN})
|
||||
idf_component_register(
|
||||
SRCS ${TT_COMPONENT_SRCS}
|
||||
INCLUDE_DIRS ${TT_COMPONENT_INCLUDE_DIRS}
|
||||
REQUIRES TactilitySDK ${TT_COMPONENT_REQUIRES}
|
||||
PRIV_REQUIRES ${TT_COMPONENT_PRIV_REQUIRES}
|
||||
)
|
||||
endmacro()
|
||||
@@ -0,0 +1,51 @@
|
||||
function(tactility_project)
|
||||
endfunction()
|
||||
|
||||
function(_tactility_project)
|
||||
endfunction()
|
||||
|
||||
macro(tactility_project_pre project_name)
|
||||
endmacro()
|
||||
|
||||
macro(tactility_project_post project_name)
|
||||
# The app's own library target is defined in a subdirectory (e.g. "main"); without this it
|
||||
# would land nested under that subdirectory instead of directly in the build dir.
|
||||
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR})
|
||||
|
||||
# Mirrors the ESP-IDF "TactilitySDK" component (Buildscripts/TactilitySDK/CMakeLists.txt):
|
||||
# apps link against this single target instead of listing SDK include dirs themselves.
|
||||
# Posix apps are dlopen()ed into a running Tactility process (see app-posix-module) and
|
||||
# resolve symbols against Tactility's own copies at load time, so headers are all they need
|
||||
# at compile time - no libraries to link.
|
||||
add_library(TactilitySDK INTERFACE)
|
||||
target_include_directories(TactilitySDK INTERFACE
|
||||
${TACTILITY_SDK_PATH}/Modules/app-module/include
|
||||
${TACTILITY_SDK_PATH}/Modules/crypt-module/include
|
||||
${TACTILITY_SDK_PATH}/Modules/gps-module/include
|
||||
${TACTILITY_SDK_PATH}/Modules/lvgl-module/include
|
||||
${TACTILITY_SDK_PATH}/Modules/lvgl-window-manager-module/include
|
||||
${TACTILITY_SDK_PATH}/Modules/service-module/include
|
||||
${TACTILITY_SDK_PATH}/Libraries/TactilityKernel/include
|
||||
${TACTILITY_SDK_PATH}/Libraries/lvgl/include
|
||||
${TACTILITY_SDK_PATH}/Libraries/FreeRTOS-Kernel/include
|
||||
${TACTILITY_SDK_PATH}/Libraries/FreeRTOS-Kernel/portable/ThirdParty/GCC/Posix
|
||||
${TACTILITY_SDK_PATH}/Libraries/FreeRTOS-Kernel/portable/ThirdParty/GCC/Posix/utils
|
||||
)
|
||||
target_compile_definitions(TactilitySDK INTERFACE LV_LVGL_H_INCLUDE_SIMPLE)
|
||||
|
||||
# ESP-IDF's project() auto-discovers the "main" component; plain CMake doesn't.
|
||||
add_subdirectory(main)
|
||||
endmacro()
|
||||
|
||||
macro(tactility_component_register)
|
||||
cmake_parse_arguments(TT_COMPONENT "" "" "SRCS;INCLUDE_DIRS;REQUIRES;PRIV_REQUIRES" ${ARGN})
|
||||
# Must be a SHARED object, not a -pie executable: glibc's dlopen() unconditionally refuses
|
||||
# any ET_DYN carrying the DF_1_PIE flag ("cannot dynamically load position-independent
|
||||
# executable"), regardless of whether it has a dynamic-linker segment - verified empirically.
|
||||
add_library(${PROJECT_NAME} SHARED ${TT_COMPONENT_SRCS})
|
||||
target_link_libraries(${PROJECT_NAME} PRIVATE TactilitySDK)
|
||||
set_target_properties(${PROJECT_NAME} PROPERTIES POSITION_INDEPENDENT_CODE ON)
|
||||
if (TT_COMPONENT_INCLUDE_DIRS)
|
||||
target_include_directories(${PROJECT_NAME} PRIVATE ${TT_COMPONENT_INCLUDE_DIRS})
|
||||
endif ()
|
||||
endmacro()
|
||||
@@ -2,6 +2,8 @@ if (COMMAND tactility_add_module)
|
||||
return()
|
||||
endif()
|
||||
|
||||
cmake_minimum_required(VERSION 3.24)
|
||||
|
||||
macro(tactility_get_module_name NAME OUT_NAME)
|
||||
if (DEFINED ENV{ESP_IDF_VERSION})
|
||||
set(${OUT_NAME} ${COMPONENT_LIB})
|
||||
@@ -16,8 +18,7 @@ macro(tactility_add_module NAME)
|
||||
# undefined reference to. Needed when this module provides symbols a component it depends on
|
||||
# (e.g. lvgl__lvgl's custom-allocator hooks) calls back into - a reverse reference a normal
|
||||
# single-pass static-archive link can't resolve, since that component is scanned after this
|
||||
# one's archive has already been passed once. POSIX builds link everything as plain OBJECT
|
||||
# libraries (no archive-pruning to begin with), so this is a no-op there.
|
||||
# one's archive has already been passed once.
|
||||
set(options WHOLE_ARCHIVE)
|
||||
set(oneValueArgs)
|
||||
set(multiValueArgs SRCS INCLUDE_DIRS PRIV_INCLUDE_DIRS REQUIRES PRIV_REQUIRES)
|
||||
@@ -40,7 +41,7 @@ macro(tactility_add_module NAME)
|
||||
${whole_archive_arg}
|
||||
)
|
||||
else()
|
||||
add_library(${NAME} OBJECT)
|
||||
add_library(${NAME} STATIC)
|
||||
target_sources(${NAME} PRIVATE ${ARG_SRCS})
|
||||
target_include_directories(${NAME}
|
||||
PRIVATE ${ARG_PRIV_INCLUDE_DIRS}
|
||||
@@ -48,5 +49,12 @@ macro(tactility_add_module NAME)
|
||||
)
|
||||
target_link_libraries(${NAME} PUBLIC ${ARG_REQUIRES})
|
||||
target_link_libraries(${NAME} PRIVATE ${ARG_PRIV_REQUIRES})
|
||||
if (ARG_WHOLE_ARCHIVE)
|
||||
# A static archive only pulls in object files that already have a pending undefined
|
||||
# reference at the point the archive is scanned, so a plain link drops ${NAME}'s
|
||||
# reverse dependencies (see WHOLE_ARCHIVE comment above). Make whoever links ${NAME}
|
||||
# whole-archive it instead of just archive-pruning it.
|
||||
set_property(TARGET ${NAME} APPEND PROPERTY INTERFACE_LINK_LIBRARIES $<LINK_LIBRARY:WHOLE_ARCHIVE,${NAME}>)
|
||||
endif()
|
||||
endif()
|
||||
endmacro()
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import os
|
||||
import platform
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
@@ -17,35 +18,24 @@ def get_idf_target():
|
||||
return None
|
||||
return None
|
||||
|
||||
def main():
|
||||
# 1. Get idf_target
|
||||
idf_target = get_idf_target()
|
||||
if not idf_target:
|
||||
print("Could not determine IDF target from sdkconfig")
|
||||
sys.exit(1)
|
||||
|
||||
# 2. Get version
|
||||
def get_version():
|
||||
try:
|
||||
with open("version.txt", "r") as f:
|
||||
version = f.read().strip()
|
||||
return f.read().strip()
|
||||
except FileNotFoundError:
|
||||
print("version.txt not found")
|
||||
sys.exit(1)
|
||||
|
||||
# 3. Construct sdk_path
|
||||
# release/TactilitySDK/${version}-${idf_target}/TactilitySDK
|
||||
sdk_path = os.path.join("release", "TactilitySDK", f"{version}-{idf_target}", "TactilitySDK")
|
||||
|
||||
# 4. Cleanup sdk_path
|
||||
def run_release_script(script_name, sdk_path):
|
||||
# Cleanup sdk_path
|
||||
if os.path.exists(sdk_path):
|
||||
print(f"Cleaning up {sdk_path}")
|
||||
shutil.rmtree(sdk_path)
|
||||
|
||||
os.makedirs(sdk_path, exist_ok=True)
|
||||
|
||||
# 5. Call release-sdk.py
|
||||
# Note: Using sys.executable to ensure we use the same python interpreter
|
||||
script_path = os.path.join("Buildscripts", "release-sdk.py")
|
||||
script_path = os.path.join("Buildscripts", script_name)
|
||||
print(f"Running {script_path} {sdk_path}")
|
||||
|
||||
result = subprocess.run([sys.executable, script_path, sdk_path])
|
||||
@@ -54,5 +44,27 @@ def main():
|
||||
print(f"Error: {script_path} failed with return code {result.returncode}")
|
||||
sys.exit(result.returncode)
|
||||
|
||||
def main():
|
||||
version = get_version()
|
||||
|
||||
# ESP_IDF_VERSION is only set once an ESP-IDF environment has been activated (export.sh /
|
||||
# the Windows PowerShell profile - see building.md); same check release-sdk-esp32.py and
|
||||
# release-sdk-posix.py themselves use to tell the two builds apart.
|
||||
esp_idf_version = os.environ.get("ESP_IDF_VERSION", "")
|
||||
|
||||
if esp_idf_version:
|
||||
idf_target = get_idf_target()
|
||||
if not idf_target:
|
||||
print("Could not determine IDF target from sdkconfig")
|
||||
sys.exit(1)
|
||||
# release/TactilitySDK/${version}-${idf_target}/TactilitySDK
|
||||
sdk_path = os.path.join("release", "TactilitySDK", f"{version}-{idf_target}", "TactilitySDK")
|
||||
run_release_script("release-sdk-esp32.py", sdk_path)
|
||||
else:
|
||||
# release/TactilitySDK/${version}-posix-${arch}/TactilitySDK
|
||||
arch = platform.machine()
|
||||
sdk_path = os.path.join("release", "TactilitySDK", f"{version}-posix-{arch}", "TactilitySDK")
|
||||
run_release_script("release-sdk-posix.py", sdk_path)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
@@ -1,65 +1,15 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import os
|
||||
import shutil
|
||||
import glob
|
||||
import subprocess
|
||||
import sys
|
||||
import importlib.util
|
||||
from textwrap import dedent
|
||||
|
||||
def map_copy(mappings, target_base):
|
||||
"""
|
||||
Helper function to map input files/directories to output files/directories.
|
||||
mappings: list of dicts with 'src' (glob pattern) and 'dst' (relative to target_base or absolute)
|
||||
'src' can be a single file or a directory (if it ends with /).
|
||||
"""
|
||||
for mapping in mappings:
|
||||
src_pattern = mapping['src']
|
||||
dst_rel = mapping['dst']
|
||||
dst_path = os.path.join(target_base, dst_rel)
|
||||
|
||||
# To preserve directory structure, we need to know where the wildcard starts
|
||||
# or have a way to determine the "base" of the search.
|
||||
# We'll split the pattern into a fixed base and a pattern part.
|
||||
|
||||
# Simple heuristic: find the first occurrence of '*' or '?'
|
||||
wildcard_idx = -1
|
||||
for i, char in enumerate(src_pattern):
|
||||
if char in '*?':
|
||||
wildcard_idx = i
|
||||
break
|
||||
|
||||
if wildcard_idx != -1:
|
||||
# Found a wildcard. The base is the directory containing it.
|
||||
pattern_base = os.path.dirname(src_pattern[:wildcard_idx])
|
||||
else:
|
||||
# No wildcard. If it's a directory, we might want to preserve its name?
|
||||
# For now, let's treat no-wildcard as no relative structure needed.
|
||||
pattern_base = None
|
||||
|
||||
src_files = glob.glob(src_pattern, recursive=True)
|
||||
if not src_files:
|
||||
continue
|
||||
|
||||
for src in src_files:
|
||||
if os.path.isdir(src):
|
||||
continue
|
||||
|
||||
if pattern_base and src.startswith(pattern_base):
|
||||
# Calculate relative path from the base of the glob pattern
|
||||
rel_src = os.path.relpath(src, pattern_base)
|
||||
# If dst_rel ends with /, it's a target directory
|
||||
if dst_rel.endswith('/') or os.path.isdir(dst_path):
|
||||
final_dst = os.path.join(dst_path, rel_src)
|
||||
else:
|
||||
# If dst_rel is a file, we can't really preserve structure
|
||||
# unless we join it. But usually it's a dir if structure is preserved.
|
||||
final_dst = dst_path
|
||||
else:
|
||||
final_dst = dst_path if not (dst_rel.endswith('/') or os.path.isdir(dst_path)) else os.path.join(dst_path, os.path.basename(src))
|
||||
|
||||
os.makedirs(os.path.dirname(final_dst), exist_ok=True)
|
||||
shutil.copy2(src, final_dst)
|
||||
_shared_spec = importlib.util.spec_from_file_location("release_sdk_shared", os.path.join("Buildscripts", "release-sdk-shared.py"))
|
||||
shared = importlib.util.module_from_spec(_shared_spec)
|
||||
_shared_spec.loader.exec_module(shared)
|
||||
|
||||
def get_driver_mappings(driver_name):
|
||||
return [
|
||||
@@ -82,11 +32,7 @@ def create_module_cmakelists(module_name):
|
||||
INCLUDE_DIRS "include"
|
||||
)
|
||||
add_prebuilt_library({module_name} "binary/lib{module_name}.a")
|
||||
'''.format(module_name=module_name))
|
||||
|
||||
def write_module_cmakelists(path, content):
|
||||
with open(path, 'w') as f:
|
||||
f.write(content)
|
||||
''')
|
||||
|
||||
def driver_is_available(driver_name):
|
||||
"""
|
||||
@@ -101,28 +47,20 @@ def driver_is_available(driver_name):
|
||||
|
||||
def add_driver(target_path, driver_name):
|
||||
mappings = get_driver_mappings(driver_name)
|
||||
map_copy(mappings, target_path)
|
||||
shared.map_copy(mappings, target_path)
|
||||
cmakelists_content = create_module_cmakelists(driver_name)
|
||||
write_module_cmakelists(os.path.join(target_path, f"Drivers/{driver_name}/CMakeLists.txt"), cmakelists_content)
|
||||
shared.write_module_cmakelists(os.path.join(target_path, f"Drivers/{driver_name}/CMakeLists.txt"), cmakelists_content)
|
||||
|
||||
def add_module(target_path, module_name):
|
||||
mappings = get_module_mappings(module_name)
|
||||
map_copy(mappings, target_path)
|
||||
shared.map_copy(mappings, target_path)
|
||||
cmakelists_content = create_module_cmakelists(module_name)
|
||||
write_module_cmakelists(os.path.join(target_path, f"Modules/{module_name}/CMakeLists.txt"), cmakelists_content)
|
||||
|
||||
def generate_tactility_sdk_cmake(target_path):
|
||||
src = os.path.join('Buildscripts', 'TactilitySDK', 'TactilitySDK.cmake')
|
||||
shutil.copy2(src, os.path.join(target_path, 'TactilitySDK.cmake'))
|
||||
|
||||
def generate_tactility_sdk_top_cmakelists(target_path):
|
||||
src = os.path.join('Buildscripts', 'TactilitySDK', 'CMakeLists.txt')
|
||||
shutil.copy2(src, os.path.join(target_path, 'CMakeLists.txt'))
|
||||
shared.write_module_cmakelists(os.path.join(target_path, f"Modules/{module_name}/CMakeLists.txt"), cmakelists_content)
|
||||
|
||||
def main():
|
||||
if len(sys.argv) < 2:
|
||||
print("Usage: release-sdk.py [target_path]")
|
||||
print("Example: release-sdk.py release/TactilitySDK")
|
||||
print("Usage: release-sdk-esp32.py [target_path]")
|
||||
print("Example: release-sdk-esp32.py release/TactilitySDK")
|
||||
sys.exit(1)
|
||||
|
||||
esp_idf_version = os.environ.get("ESP_IDF_VERSION", "")
|
||||
@@ -169,20 +107,16 @@ def main():
|
||||
{'src': 'Libraries/minmea/COPYING', 'dst': 'Libraries/minmea/'},
|
||||
]
|
||||
|
||||
map_copy(mappings, target_path)
|
||||
shared.map_copy(mappings, target_path)
|
||||
|
||||
# Modules
|
||||
add_module(target_path, "app-module")
|
||||
add_module(target_path, "crypt-module")
|
||||
add_module(target_path, "gps-module")
|
||||
add_module(target_path, "http-module")
|
||||
add_module(target_path, "lvgl-module")
|
||||
add_module(target_path, "lvgl-window-manager-module")
|
||||
add_module(target_path, "service-module")
|
||||
module_names = shared.read_module_list(os.path.join('Buildscripts', 'release-sdk-modules.txt'))
|
||||
for module_name in module_names:
|
||||
add_module(target_path, module_name)
|
||||
|
||||
# Final scripts - copied verbatim
|
||||
generate_tactility_sdk_cmake(target_path)
|
||||
generate_tactility_sdk_top_cmakelists(target_path)
|
||||
shared.generate_tactility_sdk_cmake(target_path, 'esp32')
|
||||
shared.generate_tactility_sdk_top_cmakelists(target_path)
|
||||
|
||||
# Output ESP-IDF SDK version to file
|
||||
with open(os.path.join(target_path, "idf-version.txt"), "a") as f:
|
||||
@@ -0,0 +1,7 @@
|
||||
app-module
|
||||
crypt-module
|
||||
gps-module
|
||||
http-module
|
||||
lvgl-module
|
||||
lvgl-window-manager-module
|
||||
service-module
|
||||
Executable
+101
@@ -0,0 +1,101 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import os
|
||||
import sys
|
||||
import importlib.util
|
||||
from textwrap import dedent
|
||||
|
||||
_shared_spec = importlib.util.spec_from_file_location("release_sdk_shared", os.path.join("Buildscripts", "release-sdk-shared.py"))
|
||||
shared = importlib.util.module_from_spec(_shared_spec)
|
||||
_shared_spec.loader.exec_module(shared)
|
||||
|
||||
def get_module_mappings(module_name):
|
||||
return [
|
||||
{'src': f'Modules/{module_name}/include/**', 'dst': f'Modules/{module_name}/include/'},
|
||||
{'src': f'Modules/{module_name}/*.md', 'dst': f'Modules/{module_name}/'},
|
||||
{'src': f'buildsim/Modules/{module_name}/lib{module_name}.a', 'dst': f'Modules/{module_name}/binary/lib{module_name}.a'},
|
||||
]
|
||||
|
||||
def create_module_cmakelists(module_name):
|
||||
return dedent(f'''
|
||||
cmake_minimum_required(VERSION 3.20)
|
||||
add_library({module_name} STATIC IMPORTED)
|
||||
set_target_properties({module_name} PROPERTIES
|
||||
IMPORTED_LOCATION "${{CMAKE_CURRENT_LIST_DIR}}/binary/lib{module_name}.a"
|
||||
INTERFACE_INCLUDE_DIRECTORIES "${{CMAKE_CURRENT_LIST_DIR}}/include"
|
||||
)
|
||||
''')
|
||||
|
||||
def add_module(target_path, module_name):
|
||||
mappings = get_module_mappings(module_name)
|
||||
shared.map_copy(mappings, target_path)
|
||||
cmakelists_content = create_module_cmakelists(module_name)
|
||||
shared.write_module_cmakelists(os.path.join(target_path, f"Modules/{module_name}/CMakeLists.txt"), cmakelists_content)
|
||||
|
||||
def main():
|
||||
if len(sys.argv) < 2:
|
||||
print("Usage: release-sdk-posix.py [target_path]")
|
||||
print("Example: release-sdk-posix.py release/TactilitySDK")
|
||||
sys.exit(1)
|
||||
|
||||
esp_idf_version = os.environ.get("ESP_IDF_VERSION", "")
|
||||
if esp_idf_version:
|
||||
print("Error: ESP_IDF_VERSION environment variable is set - this script packages the POSIX/simulator build, run it outside an ESP-IDF environment")
|
||||
sys.exit(1)
|
||||
|
||||
target_path = os.path.abspath(sys.argv[1])
|
||||
os.makedirs(target_path, exist_ok=True)
|
||||
|
||||
# Mapping logic
|
||||
mappings = [
|
||||
{'src': 'version.txt', 'dst': ''},
|
||||
# TactilityFreeRtos
|
||||
{'src': 'TactilityFreeRtos/Include/**', 'dst': 'Libraries/TactilityFreeRtos/Include/'},
|
||||
{'src': 'TactilityFreeRtos/CMakeLists.txt', 'dst': 'Libraries/TactilityFreeRtos/'},
|
||||
{'src': 'TactilityFreeRtos/LICENSE*.*', 'dst': 'Libraries/TactilityFreeRtos/'},
|
||||
# TactilityKernel
|
||||
{'src': 'buildsim/TactilityKernel/libTactilityKernel.a', 'dst': 'Libraries/TactilityKernel/binary/'},
|
||||
{'src': 'TactilityKernel/include/**', 'dst': 'Libraries/TactilityKernel/include/'},
|
||||
{'src': 'TactilityKernel/CMakeLists.txt', 'dst': 'Libraries/TactilityKernel/'},
|
||||
{'src': 'TactilityKernel/*.md', 'dst': 'Libraries/TactilityKernel/'},
|
||||
# FreeRTOS-Kernel - TactilityKernel's public headers (tactility/freertos/*.h) include the
|
||||
# real FreeRTOS.h/task.h/etc directly, unlike ESP32 where ESP-IDF's own "freertos"
|
||||
# component and Kconfig-generated FreeRTOSConfig.h are already part of every project.
|
||||
{'src': 'Libraries/FreeRTOS-Kernel/include/**', 'dst': 'Libraries/FreeRTOS-Kernel/include/'},
|
||||
{'src': 'Libraries/FreeRTOS-Kernel/portable/ThirdParty/GCC/Posix/*.h', 'dst': 'Libraries/FreeRTOS-Kernel/portable/ThirdParty/GCC/Posix/'},
|
||||
{'src': 'Libraries/FreeRTOS-Kernel/portable/ThirdParty/GCC/Posix/utils/*.h', 'dst': 'Libraries/FreeRTOS-Kernel/portable/ThirdParty/GCC/Posix/utils/'},
|
||||
{'src': 'Libraries/FreeRTOS-Kernel/LICENSE*.*', 'dst': 'Libraries/FreeRTOS-Kernel/'},
|
||||
{'src': 'Devices/simulator/Source/FreeRTOSConfig.h', 'dst': 'Libraries/FreeRTOS-Kernel/include/'},
|
||||
# lvgl (basics)
|
||||
{'src': 'buildsim/Libraries/lvgl/lib/liblvgl.a', 'dst': 'Libraries/lvgl/binary/liblvgl.a'},
|
||||
{'src': 'Libraries/lvgl/lvgl.h', 'dst': 'Libraries/lvgl/include/'},
|
||||
{'src': 'Libraries/lvgl/lv_version.h', 'dst': 'Libraries/lvgl/include/'},
|
||||
{'src': 'Libraries/lvgl/LICENCE*.*', 'dst': 'Libraries/lvgl/'},
|
||||
{'src': 'lv_conf.h', 'dst': 'Libraries/lvgl/include/'},
|
||||
{'src': 'Libraries/lvgl/src/**/*.h', 'dst': 'Libraries/lvgl/include/src/'},
|
||||
# minitar
|
||||
{'src': 'buildsim/Libraries/minitar/libminitar.a', 'dst': 'Libraries/minitar/binary/'},
|
||||
{'src': 'Libraries/minitar/minitar/minitar.h', 'dst': 'Libraries/minitar/include/'},
|
||||
{'src': 'Libraries/minitar/minitar/LICENSE*', 'dst': 'Libraries/minitar/'},
|
||||
# minmea
|
||||
{'src': 'buildsim/Libraries/minmea/libminmea.a', 'dst': 'Libraries/minmea/binary/'},
|
||||
{'src': 'Libraries/minmea/Include/**', 'dst': 'Libraries/minmea/include/'},
|
||||
{'src': 'Libraries/minmea/CMakeLists.txt', 'dst': 'Libraries/minmea/'},
|
||||
{'src': 'Libraries/minmea/README.md', 'dst': 'Libraries/minmea/'},
|
||||
{'src': 'Libraries/minmea/LICENSE*.*', 'dst': 'Libraries/minmea/'},
|
||||
{'src': 'Libraries/minmea/COPYING', 'dst': 'Libraries/minmea/'},
|
||||
]
|
||||
|
||||
shared.map_copy(mappings, target_path)
|
||||
|
||||
# Modules
|
||||
module_names = shared.read_module_list(os.path.join('Buildscripts', 'release-sdk-modules.txt'))
|
||||
for module_name in module_names:
|
||||
add_module(target_path, module_name)
|
||||
|
||||
# Final scripts - copied verbatim
|
||||
shared.generate_tactility_sdk_cmake(target_path, 'posix')
|
||||
shared.generate_tactility_sdk_top_cmakelists(target_path)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,91 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
# Functions shared between release-sdk-esp32.py and release-sdk-posix.py. Not runnable on its
|
||||
# own; loaded by those scripts via importlib (its hyphenated filename isn't a valid Python
|
||||
# module name for a plain "import").
|
||||
|
||||
import os
|
||||
import shutil
|
||||
import glob
|
||||
import sys
|
||||
|
||||
def map_copy(mappings, target_base):
|
||||
"""
|
||||
Helper function to map input files/directories to output files/directories.
|
||||
mappings: list of dicts with 'src' (glob pattern) and 'dst' (relative to target_base or absolute)
|
||||
'src' can be a single file or a directory (if it ends with /).
|
||||
"""
|
||||
for mapping in mappings:
|
||||
src_pattern = mapping['src']
|
||||
dst_rel = mapping['dst']
|
||||
dst_path = os.path.join(target_base, dst_rel)
|
||||
|
||||
# To preserve directory structure, we need to know where the wildcard starts
|
||||
# or have a way to determine the "base" of the search.
|
||||
# We'll split the pattern into a fixed base and a pattern part.
|
||||
|
||||
# Simple heuristic: find the first occurrence of '*' or '?'
|
||||
wildcard_idx = -1
|
||||
for i, char in enumerate(src_pattern):
|
||||
if char in '*?':
|
||||
wildcard_idx = i
|
||||
break
|
||||
|
||||
if wildcard_idx != -1:
|
||||
# Found a wildcard. The base is the directory containing it.
|
||||
pattern_base = os.path.dirname(src_pattern[:wildcard_idx])
|
||||
else:
|
||||
# No wildcard. If it's a directory, we might want to preserve its name?
|
||||
# For now, let's treat no-wildcard as no relative structure needed.
|
||||
pattern_base = None
|
||||
|
||||
src_files = glob.glob(src_pattern, recursive=True)
|
||||
if not src_files:
|
||||
continue
|
||||
|
||||
for src in src_files:
|
||||
if os.path.isdir(src):
|
||||
continue
|
||||
|
||||
if pattern_base and src.startswith(pattern_base):
|
||||
# Calculate relative path from the base of the glob pattern
|
||||
rel_src = os.path.relpath(src, pattern_base)
|
||||
# If dst_rel ends with /, it's a target directory
|
||||
if dst_rel.endswith('/') or os.path.isdir(dst_path):
|
||||
final_dst = os.path.join(dst_path, rel_src)
|
||||
else:
|
||||
# If dst_rel is a file, we can't really preserve structure
|
||||
# unless we join it. But usually it's a dir if structure is preserved.
|
||||
final_dst = dst_path
|
||||
else:
|
||||
final_dst = dst_path if not (dst_rel.endswith('/') or os.path.isdir(dst_path)) else os.path.join(dst_path, os.path.basename(src))
|
||||
|
||||
os.makedirs(os.path.dirname(final_dst), exist_ok=True)
|
||||
shutil.copy2(src, final_dst)
|
||||
|
||||
def write_module_cmakelists(path, content):
|
||||
with open(path, 'w') as f:
|
||||
f.write(content)
|
||||
|
||||
def read_module_list(path):
|
||||
"""Reads a newline-separated module name list, skipping empty lines, and checks that each
|
||||
named module actually exists under Modules/ - exits the process with an error if not."""
|
||||
with open(path, 'r') as f:
|
||||
module_names = [line.strip() for line in f if line.strip()]
|
||||
|
||||
for module_name in module_names:
|
||||
if not os.path.isdir(os.path.join('Modules', module_name)):
|
||||
print(f"Error: Modules/{module_name} does not exist (listed in {path})")
|
||||
sys.exit(1)
|
||||
|
||||
return module_names
|
||||
|
||||
def generate_tactility_sdk_cmake(target_path, variant):
|
||||
"""variant selects Buildscripts/TactilitySDK/TactilitySDK.{variant}.cmake (e.g. "esp32" or
|
||||
"posix") - always copied into the SDK as the platform-neutral name TactilitySDK.cmake."""
|
||||
src = os.path.join('Buildscripts', 'TactilitySDK', f'TactilitySDK.{variant}.cmake')
|
||||
shutil.copy2(src, os.path.join(target_path, 'TactilitySDK.cmake'))
|
||||
|
||||
def generate_tactility_sdk_top_cmakelists(target_path):
|
||||
src = os.path.join('Buildscripts', 'TactilitySDK', 'CMakeLists.txt')
|
||||
shutil.copy2(src, os.path.join(target_path, 'CMakeLists.txt'))
|
||||
@@ -129,6 +129,7 @@ if (NOT DEFINED ENV{ESP_IDF_VERSION})
|
||||
add_subdirectory(Modules/pthread-module)
|
||||
add_subdirectory(Modules/service-module)
|
||||
add_subdirectory(Modules/app-module)
|
||||
add_subdirectory(Modules/app-posix-module)
|
||||
add_subdirectory(Modules/lvgl-window-manager-module)
|
||||
add_subdirectory(Drivers/gps-generic-module)
|
||||
add_subdirectory(Drivers/gps-meshtastic-module)
|
||||
|
||||
@@ -15,50 +15,46 @@ constexpr auto* TAG = "SdlDisplay";
|
||||
#define GET_CONFIG(device) (static_cast<const SdlDisplayConfig*>((device)->config))
|
||||
|
||||
struct SdlDisplayInternal {
|
||||
bool initialized;
|
||||
bool init_failed;
|
||||
SDL_Window* window;
|
||||
SDL_Renderer* renderer;
|
||||
SDL_Texture* texture;
|
||||
};
|
||||
|
||||
// Only one sdl-display device exists in the simulator; sdl_input.cpp uses this to map window
|
||||
// coordinates back to the fixed logical resolution SDL_RenderSetLogicalSize() scales to the window.
|
||||
static SdlDisplayInternal* g_display_internal = nullptr;
|
||||
|
||||
SDL_Renderer* sdl_display_get_renderer(void) {
|
||||
return g_display_internal != nullptr ? g_display_internal->renderer : nullptr;
|
||||
}
|
||||
|
||||
// Re-blits the already-drawn texture at the renderer's current (possibly just-resized) scale.
|
||||
// No new pixel data needed: the window resizing doesn't change what LVGL last rendered, only how
|
||||
// large it should appear, and SDL only applies that until the next SDL_RenderPresent() call.
|
||||
void sdl_display_present_now(void) {
|
||||
if (g_display_internal == nullptr) {
|
||||
return;
|
||||
}
|
||||
SDL_RenderClear(g_display_internal->renderer);
|
||||
SDL_RenderCopy(g_display_internal->renderer, g_display_internal->texture, nullptr, nullptr);
|
||||
SDL_RenderPresent(g_display_internal->renderer);
|
||||
}
|
||||
|
||||
// region Driver lifecycle
|
||||
|
||||
static error_t start(Device* device) {
|
||||
const auto* config = GET_CONFIG(device);
|
||||
|
||||
auto* internal = static_cast<SdlDisplayInternal*>(malloc(sizeof(SdlDisplayInternal)));
|
||||
if (internal == nullptr) {
|
||||
return ERROR_OUT_OF_MEMORY;
|
||||
}
|
||||
|
||||
if (SDL_InitSubSystem(SDL_INIT_VIDEO) != 0) {
|
||||
LOG_E(TAG, "SDL_InitSubSystem failed: %s", SDL_GetError());
|
||||
free(internal);
|
||||
return ERROR_RESOURCE;
|
||||
}
|
||||
|
||||
internal->window = SDL_CreateWindow(
|
||||
"Tactility",
|
||||
SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED,
|
||||
config->horizontal_resolution, config->vertical_resolution,
|
||||
SDL_WINDOW_SHOWN
|
||||
);
|
||||
internal->renderer = internal->window != nullptr
|
||||
? SDL_CreateRenderer(internal->window, -1, SDL_RENDERER_ACCELERATED)
|
||||
: nullptr;
|
||||
internal->texture = internal->renderer != nullptr
|
||||
? SDL_CreateTexture(internal->renderer, SDL_PIXELFORMAT_RGB565, SDL_TEXTUREACCESS_STREAMING,
|
||||
config->horizontal_resolution, config->vertical_resolution)
|
||||
: nullptr;
|
||||
|
||||
if (internal->window == nullptr || internal->renderer == nullptr || internal->texture == nullptr) {
|
||||
LOG_E(TAG, "Failed to create SDL window: %s", SDL_GetError());
|
||||
if (internal->texture != nullptr) SDL_DestroyTexture(internal->texture);
|
||||
if (internal->renderer != nullptr) SDL_DestroyRenderer(internal->renderer);
|
||||
if (internal->window != nullptr) SDL_DestroyWindow(internal->window);
|
||||
SDL_QuitSubSystem(SDL_INIT_VIDEO);
|
||||
free(internal);
|
||||
return ERROR_RESOURCE;
|
||||
}
|
||||
internal->initialized = false;
|
||||
internal->init_failed = false;
|
||||
internal->window = nullptr;
|
||||
internal->renderer = nullptr;
|
||||
internal->texture = nullptr;
|
||||
|
||||
device_set_driver_data(device, internal);
|
||||
return ERROR_NONE;
|
||||
@@ -67,10 +63,16 @@ static error_t start(Device* device) {
|
||||
static error_t stop(Device* device) {
|
||||
auto* internal = static_cast<SdlDisplayInternal*>(device_get_driver_data(device));
|
||||
|
||||
if (internal->initialized) {
|
||||
SDL_DestroyTexture(internal->texture);
|
||||
SDL_DestroyRenderer(internal->renderer);
|
||||
SDL_DestroyWindow(internal->window);
|
||||
SDL_QuitSubSystem(SDL_INIT_VIDEO);
|
||||
}
|
||||
|
||||
if (g_display_internal == internal) {
|
||||
g_display_internal = nullptr;
|
||||
}
|
||||
|
||||
free(internal);
|
||||
device_set_driver_data(device, nullptr);
|
||||
@@ -84,18 +86,83 @@ static error_t stop(Device* device) {
|
||||
static error_t sdl_display_reset(Device*) { return ERROR_NONE; }
|
||||
static error_t sdl_display_init(Device*) { return ERROR_NONE; }
|
||||
|
||||
static float sdl_display_get_dpi_scale() {
|
||||
float hdpi = 96.0f;
|
||||
if (SDL_GetDisplayDPI(0, nullptr, &hdpi, nullptr) != 0 || hdpi <= 0.0f) {
|
||||
return 1.0f;
|
||||
}
|
||||
return hdpi / 96.0f;
|
||||
}
|
||||
|
||||
static bool sdl_display_lazy_init(Device* device, SdlDisplayInternal* internal) {
|
||||
const auto* config = GET_CONFIG(device);
|
||||
|
||||
if (SDL_InitSubSystem(SDL_INIT_VIDEO) != 0) {
|
||||
LOG_E(TAG, "SDL_InitSubSystem failed: %s", SDL_GetError());
|
||||
return false;
|
||||
}
|
||||
|
||||
// Only the window's initial on-screen footprint scales here - the render/logical resolution
|
||||
// (config->horizontal_resolution/vertical_resolution, used below for the texture and
|
||||
// SDL_RenderSetLogicalSize()) is unaffected, same as any other resize the user does by hand.
|
||||
const float dpi_scale = sdl_display_get_dpi_scale();
|
||||
const int initial_width = static_cast<int>(config->horizontal_resolution * dpi_scale);
|
||||
const int initial_height = static_cast<int>(config->vertical_resolution * dpi_scale);
|
||||
|
||||
internal->window = SDL_CreateWindow(
|
||||
"Tactility",
|
||||
SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED,
|
||||
initial_width, initial_height,
|
||||
SDL_WINDOW_SHOWN | SDL_WINDOW_RESIZABLE | SDL_WINDOW_ALLOW_HIGHDPI
|
||||
);
|
||||
internal->renderer = internal->window != nullptr
|
||||
? SDL_CreateRenderer(internal->window, -1, SDL_RENDERER_ACCELERATED)
|
||||
: nullptr;
|
||||
// Lets the window be resized freely while the renderer scales/letterboxes the fixed-resolution
|
||||
// texture below to fit - LVGL keeps rendering at horizontal_resolution x vertical_resolution.
|
||||
if (internal->renderer != nullptr) {
|
||||
SDL_RenderSetLogicalSize(internal->renderer, config->horizontal_resolution, config->vertical_resolution);
|
||||
}
|
||||
internal->texture = internal->renderer != nullptr
|
||||
? SDL_CreateTexture(internal->renderer, SDL_PIXELFORMAT_RGB565, SDL_TEXTUREACCESS_STREAMING,
|
||||
config->horizontal_resolution, config->vertical_resolution)
|
||||
: nullptr;
|
||||
|
||||
if (internal->window == nullptr || internal->renderer == nullptr || internal->texture == nullptr) {
|
||||
LOG_E(TAG, "Failed to create SDL window: %s", SDL_GetError());
|
||||
if (internal->texture != nullptr) SDL_DestroyTexture(internal->texture);
|
||||
if (internal->renderer != nullptr) SDL_DestroyRenderer(internal->renderer);
|
||||
if (internal->window != nullptr) SDL_DestroyWindow(internal->window);
|
||||
SDL_QuitSubSystem(SDL_INIT_VIDEO);
|
||||
return false;
|
||||
}
|
||||
|
||||
g_display_internal = internal;
|
||||
return true;
|
||||
}
|
||||
|
||||
static error_t sdl_display_draw_bitmap(Device* device, int32_t x_start, int32_t y_start, int32_t x_end, int32_t y_end, const void* color_data) {
|
||||
auto* internal = static_cast<SdlDisplayInternal*>(device_get_driver_data(device));
|
||||
|
||||
if (internal->init_failed) {
|
||||
return ERROR_RESOURCE;
|
||||
}
|
||||
|
||||
if (!internal->initialized) {
|
||||
if (!sdl_display_lazy_init(device, internal)) {
|
||||
internal->init_failed = true;
|
||||
return ERROR_RESOURCE;
|
||||
}
|
||||
internal->initialized = true;
|
||||
}
|
||||
|
||||
SDL_Rect rect = { x_start, y_start, x_end - x_start, y_end - y_start };
|
||||
// RGB565 = 2 bytes/pixel.
|
||||
if (SDL_UpdateTexture(internal->texture, &rect, color_data, (x_end - x_start) * 2) != 0) {
|
||||
return ERROR_RESOURCE;
|
||||
}
|
||||
|
||||
SDL_RenderClear(internal->renderer);
|
||||
SDL_RenderCopy(internal->renderer, internal->texture, nullptr, nullptr);
|
||||
SDL_RenderPresent(internal->renderer);
|
||||
sdl_display_present_now();
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#pragma once
|
||||
|
||||
#include <SDL2/SDL.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
@@ -12,6 +14,22 @@ struct SdlDisplayConfig {
|
||||
uint16_t vertical_resolution;
|
||||
};
|
||||
|
||||
/**
|
||||
* @return the display's renderer, or NULL if the display hasn't drawn its first frame yet
|
||||
* (see sdl_display_lazy_init() in sdl_display.cpp). Used by sdl_input.cpp to map window
|
||||
* coordinates back to the fixed logical resolution the renderer scales to fit the window.
|
||||
*/
|
||||
SDL_Renderer* sdl_display_get_renderer(void);
|
||||
|
||||
/**
|
||||
* @brief Re-presents the already-drawn frame at the renderer's current scale. Call this when the
|
||||
* window is resized: the window size change alone doesn't make SDL re-blit the last frame at
|
||||
* the new scale until something calls SDL_RenderPresent() again, and LVGL won't do that on
|
||||
* its own since nothing it's tracking actually changed. No-op if the display hasn't drawn its
|
||||
* first frame yet.
|
||||
*/
|
||||
void sdl_display_present_now(void);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#include "sdl_input.h"
|
||||
#include "sdl_display.h"
|
||||
|
||||
#include <tactility/drivers/keyboard.h>
|
||||
|
||||
@@ -34,6 +35,23 @@ void push_key(uint32_t key) {
|
||||
// all of these back to LVGL's own sentinels (CODEPOINT_ESCAPE/BACKSPACE/DELETE already equal their
|
||||
// LV_KEY_* counterpart numerically, so no translation is needed for those). Printable characters
|
||||
// arrive separately via SDL_TEXTINPUT.
|
||||
// The window can be freely resized (see sdl_display.cpp's SDL_WINDOW_RESIZABLE/
|
||||
// SDL_RenderSetLogicalSize()), so raw SDL mouse coordinates are in window-pixel space, not the
|
||||
// fixed logical resolution LVGL renders at. SDL_RenderWindowToLogical() is the renderer's own
|
||||
// inverse of that scaling, accounting for both the scale factor and any letterbox offset.
|
||||
void set_pointer_position(int32_t window_x, int32_t window_y) {
|
||||
SDL_Renderer* renderer = sdl_display_get_renderer();
|
||||
if (renderer == nullptr) {
|
||||
pointer_state.x = window_x;
|
||||
pointer_state.y = window_y;
|
||||
return;
|
||||
}
|
||||
float logical_x, logical_y;
|
||||
SDL_RenderWindowToLogical(renderer, window_x, window_y, &logical_x, &logical_y);
|
||||
pointer_state.x = static_cast<int32_t>(logical_x);
|
||||
pointer_state.y = static_cast<int32_t>(logical_y);
|
||||
}
|
||||
|
||||
uint32_t keycode_to_key(SDL_Keycode sdl_key, bool shift) {
|
||||
switch (sdl_key) {
|
||||
case SDLK_RIGHT: return CODEPOINT_ARROW_RIGHT;
|
||||
@@ -64,13 +82,17 @@ void sdl_input_pump() {
|
||||
while (SDL_PollEvent(&event)) {
|
||||
switch (event.type) {
|
||||
case SDL_MOUSEMOTION:
|
||||
pointer_state.x = event.motion.x;
|
||||
pointer_state.y = event.motion.y;
|
||||
set_pointer_position(event.motion.x, event.motion.y);
|
||||
break;
|
||||
case SDL_MOUSEBUTTONDOWN:
|
||||
if (event.button.button == SDL_BUTTON_LEFT) {
|
||||
pointer_state.x = event.button.x;
|
||||
pointer_state.y = event.button.y;
|
||||
// event.button.x/y can be stale immediately after a window resize (an
|
||||
// SDL/X11 event-queue quirk - confirmed by comparing against a live
|
||||
// SDL_GetWindowSize() at the same instant). SDL_GetMouseState() queries the
|
||||
// OS for the current pointer position directly, sidestepping that entirely.
|
||||
int live_x, live_y;
|
||||
SDL_GetMouseState(&live_x, &live_y);
|
||||
set_pointer_position(live_x, live_y);
|
||||
pointer_state.pressed = true;
|
||||
}
|
||||
break;
|
||||
@@ -86,6 +108,14 @@ void sdl_input_pump() {
|
||||
// ASCII only (first byte of event.text.text) - sufficient for a simulator keyboard.
|
||||
push_key(static_cast<uint8_t>(event.text.text[0]));
|
||||
break;
|
||||
case SDL_WINDOWEVENT:
|
||||
// Resizing doesn't change what LVGL last rendered, only how large it should
|
||||
// appear - re-present the existing frame at the new scale immediately, rather
|
||||
// than leaving stale-looking content on screen until the next LVGL-driven flush.
|
||||
if (event.window.event == SDL_WINDOWEVENT_SIZE_CHANGED) {
|
||||
sdl_display_present_now();
|
||||
}
|
||||
break;
|
||||
case SDL_QUIT:
|
||||
exit(0);
|
||||
default:
|
||||
|
||||
@@ -29,7 +29,7 @@ static Driver* const simulator_drivers[] = {
|
||||
// These devices have no real bus to attach to (SDL has no notion of one), but every non-root
|
||||
// device is still expected to have a parent (see Device::parent) - they're parented to root once
|
||||
// it's available below.
|
||||
static const SdlDisplayConfig sdl_display_config = { 320, 240 };
|
||||
static const SdlDisplayConfig sdl_display_config = { 640, 480 };
|
||||
static Device sdl_display_device {};
|
||||
static Device sdl_pointer_device {};
|
||||
static Device sdl_keyboard_device {};
|
||||
|
||||
@@ -27,11 +27,7 @@ static error_t start(Device* device) {
|
||||
auto* parent = device_get_parent(device);
|
||||
check(device_get_type(parent) == &I2C_CONTROLLER_TYPE);
|
||||
|
||||
auto address = GET_CONFIG(device)->address;
|
||||
if (i2c_controller_has_device_at_address(parent, address, I2C_TIMEOUT) != ERROR_NONE) {
|
||||
LOG_E(TAG, "No device found on I2C bus at address 0x%02X", address);
|
||||
return ERROR_RESOURCE;
|
||||
}
|
||||
// We don't check whether the device is present, because it doesn't respond reliably at boot
|
||||
|
||||
auto* internal = static_cast<TdeckKeyboardInternal*>(malloc(sizeof(TdeckKeyboardInternal)));
|
||||
if (internal == nullptr) {
|
||||
|
||||
@@ -28,11 +28,7 @@ static error_t start(Device* device) {
|
||||
auto* parent = device_get_parent(device);
|
||||
check(device_get_type(parent) == &I2C_CONTROLLER_TYPE);
|
||||
|
||||
auto address = GET_CONFIG(device)->address;
|
||||
if (i2c_controller_has_device_at_address(parent, address, I2C_TIMEOUT) != ERROR_NONE) {
|
||||
LOG_E(TAG, "No device found on I2C bus at address 0x%02X", address);
|
||||
return ERROR_RESOURCE;
|
||||
}
|
||||
// We don't check whether the device is present, because it doesn't respond reliably at boot
|
||||
|
||||
auto* internal = static_cast<TdeckKeyboardBacklightInternal*>(malloc(sizeof(TdeckKeyboardBacklightInternal)));
|
||||
if (internal == nullptr) {
|
||||
@@ -43,6 +39,8 @@ static error_t start(Device* device) {
|
||||
|
||||
auto brightness_default = GET_CONFIG(device)->brightness_default;
|
||||
|
||||
auto address = GET_CONFIG(device)->address;
|
||||
|
||||
// Configures the keyboard controller's own persisted default, used by its onboard ALT+B toggle.
|
||||
if (i2c_controller_write_register(parent, address, CMD_DEFAULT_BRIGHTNESS, &brightness_default, 1, I2C_TIMEOUT) != ERROR_NONE) {
|
||||
LOG_E(TAG, "Failed to set default brightness");
|
||||
|
||||
@@ -10,3 +10,12 @@ tactility_add_module(app-module
|
||||
INCLUDE_DIRS include/
|
||||
REQUIRES TactilityKernel service-module minitar
|
||||
)
|
||||
|
||||
# Tells source/io.cpp its real-syscall fallback must go through __real_read/write/close()
|
||||
# rather than calling ::read/::write/::close() directly, on every platform whose build wraps
|
||||
# those symbols (ESP-IDF always; POSIX except macOS, whose linker doesn't support --wrap) -
|
||||
# see Tactility/CMakeLists.txt and the top-level CMakeLists.txt for where that's applied.
|
||||
if (NOT APPLE)
|
||||
tactility_get_module_name(app-module MODULE_NAME)
|
||||
target_compile_definitions(${MODULE_NAME} PRIVATE TT_APP_IO_WRAPS_STDIO)
|
||||
endif ()
|
||||
|
||||
@@ -111,6 +111,23 @@ struct AppStreamBinding {
|
||||
*/
|
||||
error_t app_manager_start_with_streams(const char* id, const struct AppStreamBinding* bindings, size_t binding_count, AppInstanceId* out_app_instance_id);
|
||||
|
||||
/**
|
||||
* Combines app_manager_start_for_result() and app_manager_start_with_streams(): starts @a id as
|
||||
* a modal child of @a parent_instance_id (see app_manager_start_for_result()'s own doc for the
|
||||
* result-delivery contract) with @a bindings installed into its fd table before its task begins
|
||||
* executing (see app_manager_start_with_streams()'s own doc for stream ownership). For a child
|
||||
* that needs to hand back more than an int32_t (e.g. a path) via its own stdout instead of the
|
||||
* "get last result" getter pattern (see app_manager_start_for_result()) - see e.g.
|
||||
* tt::app::fileselection::startForExistingFile().
|
||||
* @param[in] argv see app_manager_start_for_result().
|
||||
* @param[in] bindings see app_manager_start_with_streams().
|
||||
* @retval ERROR_NOT_FOUND no manifest with this id is registered, or no AppLoaderApi is registered
|
||||
* @retval ERROR_OUT_OF_RANGE a binding's producer_fd is out of range
|
||||
* @retval ERROR_RESOURCE a binding's event_group has no free bits left to claim
|
||||
* @retval ERROR_NONE on success
|
||||
*/
|
||||
error_t app_manager_start_for_result_with_streams(const char* id, AppInstanceId parent_instance_id, int argc, const char* const argv[], const struct AppStreamBinding* bindings, size_t binding_count, AppInstanceId* out_app_instance_id);
|
||||
|
||||
/**
|
||||
* Stop an app instance permanently. Emits APP_EVENT_CLOSE and bound-waits for its task to exit
|
||||
* if it was running.
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
#include <cerrno>
|
||||
|
||||
#ifdef ESP_PLATFORM
|
||||
#if defined(TT_APP_IO_WRAPS_STDIO)
|
||||
extern "C" {
|
||||
ssize_t __real_read(int fd, void* buffer, size_t size);
|
||||
ssize_t __real_write(int fd, const void* buffer, size_t size);
|
||||
@@ -56,7 +56,7 @@ ssize_t app_io_read(int fd, void* buffer, size_t size) {
|
||||
errno = EBADF;
|
||||
return -1;
|
||||
}
|
||||
#ifdef ESP_PLATFORM
|
||||
#if defined(TT_APP_IO_WRAPS_STDIO)
|
||||
return __real_read(fd, buffer, size);
|
||||
#else
|
||||
return ::read(fd, buffer, size);
|
||||
@@ -71,13 +71,25 @@ ssize_t app_io_write(int fd, const void* buffer, size_t size) {
|
||||
if (file.ops->release != nullptr) {
|
||||
file.ops->release(file.object);
|
||||
}
|
||||
// Tee to the real fd too: a bound stream only exists because a parent explicitly asked
|
||||
// to capture this app instance's own output (see AppStreamBinding), but generic code
|
||||
// running on that same instance's thread - most commonly the platform's own logging
|
||||
// (LOG_I/etc, which calls write() the same as anything else) - has no way to know its
|
||||
// output is currently being intercepted. Without this, a log line emitted while any app
|
||||
// instance has its stdout captured would vanish from the console entirely instead of
|
||||
// just also being visible to the capturing parent.
|
||||
#if defined(TT_APP_IO_WRAPS_STDIO)
|
||||
__real_write(fd, buffer, size);
|
||||
#else
|
||||
::write(fd, buffer, size);
|
||||
#endif
|
||||
return result;
|
||||
}
|
||||
if (table != nullptr && app_fd_table_is_app_owned(table, fd)) {
|
||||
errno = EBADF;
|
||||
return -1;
|
||||
}
|
||||
#ifdef ESP_PLATFORM
|
||||
#if defined(TT_APP_IO_WRAPS_STDIO)
|
||||
return __real_write(fd, buffer, size);
|
||||
#else
|
||||
return ::write(fd, buffer, size);
|
||||
@@ -96,7 +108,7 @@ int app_io_close(int fd) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
#ifdef ESP_PLATFORM
|
||||
#if defined(TT_APP_IO_WRAPS_STDIO)
|
||||
return __real_close(fd);
|
||||
#else
|
||||
return ::close(fd);
|
||||
|
||||
@@ -178,6 +178,10 @@ error_t app_manager_start_with_streams(const char* id, const AppStreamBinding* b
|
||||
return start_internal(id, 0, 0, nullptr, bindings, binding_count, out_app_instance_id);
|
||||
}
|
||||
|
||||
error_t app_manager_start_for_result_with_streams(const char* id, AppInstanceId parent_instance_id, int argc, const char* const argv[], const AppStreamBinding* bindings, size_t binding_count, AppInstanceId* out_app_instance_id) {
|
||||
return start_internal(id, parent_instance_id, argc, copy_arguments(argc, argv), bindings, binding_count, out_app_instance_id);
|
||||
}
|
||||
|
||||
error_t app_manager_stop(AppInstanceId app_instance_id) {
|
||||
return app_scheduler_stop(app_instance_id, pdMS_TO_TICKS(2000));
|
||||
}
|
||||
|
||||
@@ -38,6 +38,7 @@ static const ModuleSymbol SYMBOLS[] = {
|
||||
DEFINE_MODULE_SYMBOL(app_manager_start_with_parameters),
|
||||
DEFINE_MODULE_SYMBOL(app_manager_start_for_result),
|
||||
DEFINE_MODULE_SYMBOL(app_manager_start_with_streams),
|
||||
DEFINE_MODULE_SYMBOL(app_manager_start_for_result_with_streams),
|
||||
DEFINE_MODULE_SYMBOL(app_manager_stop),
|
||||
DEFINE_MODULE_SYMBOL(app_manager_get_state),
|
||||
DEFINE_MODULE_SYMBOL(app_manager_find_manifest),
|
||||
|
||||
@@ -199,12 +199,19 @@ void app_task_main(void* context) {
|
||||
check(pvTaskGetThreadLocalStoragePointer(nullptr, APP_INSTANCE_ID_THREAD_SLOT_INDEX) == nullptr);
|
||||
vTaskSetThreadLocalStoragePointer(nullptr, APP_INSTANCE_ID_THREAD_SLOT_INDEX, reinterpret_cast<void*>(static_cast<uintptr_t>(ctx->app_instance_id)));
|
||||
|
||||
// Debug logging so it's invisible by default
|
||||
// When logging happens, it can distort the application stdout, which breaks apps that use
|
||||
// stdout to output important information, such as the file selection dialog app.
|
||||
LOG_I(TAG, "[instance %lu] Task started", ctx->app_instance_id);
|
||||
|
||||
set_state(ctx->app_instance_id, APP_INSTANCE_STATE_ACTIVE);
|
||||
|
||||
int32_t result = ctx->loader->run(ctx->runtime, ctx->app_instance_id, ctx->argc, ctx->argv);
|
||||
|
||||
// The platform might buffer stdout (e.g. esp-idf with newlib)
|
||||
// Do a manual flush to ensure data has been written:
|
||||
fflush(stdout);
|
||||
|
||||
vTaskSetThreadLocalStoragePointer(nullptr, APP_INSTANCE_ID_THREAD_SLOT_INDEX, nullptr);
|
||||
|
||||
ctx->loader->unload(ctx->runtime);
|
||||
|
||||
@@ -6,10 +6,31 @@ enable_language(C CXX ASM)
|
||||
file(GLOB_RECURSE TEST_SOURCES CONFIGURE_DEPENDS ${PROJECT_SOURCE_DIR}/source/*.cpp)
|
||||
add_executable(AppModuleTests EXCLUDE_FROM_ALL ${TEST_SOURCES})
|
||||
|
||||
if (NOT APPLE)
|
||||
# Provides __wrap_read/write/close for the -Wl,--wrap= below (see Tactility/CMakeLists.txt
|
||||
# for the canonical pairing of this file with those flags).
|
||||
target_sources(AppModuleTests PRIVATE ${CMAKE_CURRENT_LIST_DIR}/../../../Tactility/Source/AppStdioWrap.cpp)
|
||||
endif ()
|
||||
|
||||
target_include_directories(AppModuleTests PRIVATE ${DOCTESTINC})
|
||||
|
||||
add_test(NAME AppModuleTests COMMAND AppModuleTests)
|
||||
|
||||
# Matches app-module's own TT_APP_IO_WRAPS_STDIO (see Modules/app-module/CMakeLists.txt):
|
||||
# io.cpp calls __real_read/write/close() on non-Apple POSIX, so any final link of it needs
|
||||
# these wraps too, or those symbols go unresolved. The printf-family/getc-family flags are
|
||||
# needed for the same reason: AppStdioWrap.cpp compiles those __wrap_* functions unconditionally
|
||||
# on this platform (see its own #if guard), and they reference __real_vfprintf/fputs/fputc/
|
||||
# fgetc/fgets - those only exist once the matching --wrap flag is passed.
|
||||
if (NOT APPLE)
|
||||
target_link_options(AppModuleTests PRIVATE
|
||||
"-Wl,--wrap=read" "-Wl,--wrap=write" "-Wl,--wrap=close"
|
||||
"-Wl,--wrap=printf" "-Wl,--wrap=fprintf" "-Wl,--wrap=vprintf" "-Wl,--wrap=vfprintf"
|
||||
"-Wl,--wrap=puts" "-Wl,--wrap=fputs" "-Wl,--wrap=putchar" "-Wl,--wrap=fputc"
|
||||
"-Wl,--wrap=getchar" "-Wl,--wrap=fgetc" "-Wl,--wrap=fgets"
|
||||
)
|
||||
endif ()
|
||||
|
||||
target_link_libraries(AppModuleTests PUBLIC
|
||||
TactilityKernel
|
||||
app-module
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
cmake_minimum_required(VERSION 3.20)
|
||||
|
||||
include("${CMAKE_CURRENT_LIST_DIR}/../../Buildscripts/module.cmake")
|
||||
|
||||
file(GLOB_RECURSE SOURCE_FILES "source/*.c*")
|
||||
|
||||
tactility_add_module(app-posix-module
|
||||
SRCS ${SOURCE_FILES}
|
||||
INCLUDE_DIRS include/
|
||||
REQUIRES TactilityKernel app-module service-module
|
||||
PRIV_REQUIRES ${CMAKE_DL_LIBS}
|
||||
)
|
||||
|
||||
# Baked in at compile time, mirroring ESP32's CONFIG_IDF_TARGET - names the installed-app binary
|
||||
# (e.g. "elf/posix-x86_64.so") this Tactility process's own architecture can dlopen(), letting one
|
||||
# .app package bundle a variant per posix architecture, same as one .app bundles a .elf per chip.
|
||||
target_compile_definitions(app-posix-module PRIVATE "TACTILITY_POSIX_ARCH=\"${CMAKE_SYSTEM_PROCESSOR}\"")
|
||||
@@ -0,0 +1,195 @@
|
||||
Apache License
|
||||
==============
|
||||
|
||||
_Version 2.0, January 2004_
|
||||
_<<http://www.apache.org/licenses/>>_
|
||||
|
||||
### Terms and Conditions for use, reproduction, and distribution
|
||||
|
||||
#### 1. Definitions
|
||||
|
||||
“License” shall mean the terms and conditions for use, reproduction, and
|
||||
distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
“Licensor” shall mean the copyright owner or entity authorized by the copyright
|
||||
owner that is granting the License.
|
||||
|
||||
“Legal Entity” shall mean the union of the acting entity and all other entities
|
||||
that control, are controlled by, or are under common control with that entity.
|
||||
For the purposes of this definition, “control” means **(i)** the power, direct or
|
||||
indirect, to cause the direction or management of such entity, whether by
|
||||
contract or otherwise, or **(ii)** ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or **(iii)** beneficial ownership of such entity.
|
||||
|
||||
“You” (or “Your”) shall mean an individual or Legal Entity exercising
|
||||
permissions granted by this License.
|
||||
|
||||
“Source” form shall mean the preferred form for making modifications, including
|
||||
but not limited to software source code, documentation source, and configuration
|
||||
files.
|
||||
|
||||
“Object” form shall mean any form resulting from mechanical transformation or
|
||||
translation of a Source form, including but not limited to compiled object code,
|
||||
generated documentation, and conversions to other media types.
|
||||
|
||||
“Work” shall mean the work of authorship, whether in Source or Object form, made
|
||||
available under the License, as indicated by a copyright notice that is included
|
||||
in or attached to the work (an example is provided in the Appendix below).
|
||||
|
||||
“Derivative Works” shall mean any work, whether in Source or Object form, that
|
||||
is based on (or derived from) the Work and for which the editorial revisions,
|
||||
annotations, elaborations, or other modifications represent, as a whole, an
|
||||
original work of authorship. For the purposes of this License, Derivative Works
|
||||
shall not include works that remain separable from, or merely link (or bind by
|
||||
name) to the interfaces of, the Work and Derivative Works thereof.
|
||||
|
||||
“Contribution” shall mean any work of authorship, including the original version
|
||||
of the Work and any modifications or additions to that Work or Derivative Works
|
||||
thereof, that is intentionally submitted to Licensor for inclusion in the Work
|
||||
by the copyright owner or by an individual or Legal Entity authorized to submit
|
||||
on behalf of the copyright owner. For the purposes of this definition,
|
||||
“submitted” means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems, and
|
||||
issue tracking systems that are managed by, or on behalf of, the Licensor for
|
||||
the purpose of discussing and improving the Work, but excluding communication
|
||||
that is conspicuously marked or otherwise designated in writing by the copyright
|
||||
owner as “Not a Contribution.”
|
||||
|
||||
“Contributor” shall mean Licensor and any individual or Legal Entity on behalf
|
||||
of whom a Contribution has been received by Licensor and subsequently
|
||||
incorporated within the Work.
|
||||
|
||||
#### 2. Grant of Copyright License
|
||||
|
||||
Subject to the terms and conditions of this License, each Contributor hereby
|
||||
grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
|
||||
irrevocable copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the Work and such
|
||||
Derivative Works in Source or Object form.
|
||||
|
||||
#### 3. Grant of Patent License
|
||||
|
||||
Subject to the terms and conditions of this License, each Contributor hereby
|
||||
grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
|
||||
irrevocable (except as stated in this section) patent license to make, have
|
||||
made, use, offer to sell, sell, import, and otherwise transfer the Work, where
|
||||
such license applies only to those patent claims licensable by such Contributor
|
||||
that are necessarily infringed by their Contribution(s) alone or by combination
|
||||
of their Contribution(s) with the Work to which such Contribution(s) was
|
||||
submitted. If You institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work or a
|
||||
Contribution incorporated within the Work constitutes direct or contributory
|
||||
patent infringement, then any patent licenses granted to You under this License
|
||||
for that Work shall terminate as of the date such litigation is filed.
|
||||
|
||||
#### 4. Redistribution
|
||||
|
||||
You may reproduce and distribute copies of the Work or Derivative Works thereof
|
||||
in any medium, with or without modifications, and in Source or Object form,
|
||||
provided that You meet the following conditions:
|
||||
|
||||
* **(a)** You must give any other recipients of the Work or Derivative Works a copy of
|
||||
this License; and
|
||||
* **(b)** You must cause any modified files to carry prominent notices stating that You
|
||||
changed the files; and
|
||||
* **(c)** You must retain, in the Source form of any Derivative Works that You distribute,
|
||||
all copyright, patent, trademark, and attribution notices from the Source form
|
||||
of the Work, excluding those notices that do not pertain to any part of the
|
||||
Derivative Works; and
|
||||
* **(d)** If the Work includes a “NOTICE” text file as part of its distribution, then any
|
||||
Derivative Works that You distribute must include a readable copy of the
|
||||
attribution notices contained within such NOTICE file, excluding those notices
|
||||
that do not pertain to any part of the Derivative Works, in at least one of the
|
||||
following places: within a NOTICE text file distributed as part of the
|
||||
Derivative Works; within the Source form or documentation, if provided along
|
||||
with the Derivative Works; or, within a display generated by the Derivative
|
||||
Works, if and wherever such third-party notices normally appear. The contents of
|
||||
the NOTICE file are for informational purposes only and do not modify the
|
||||
License. You may add Your own attribution notices within Derivative Works that
|
||||
You distribute, alongside or as an addendum to the NOTICE text from the Work,
|
||||
provided that such additional attribution notices cannot be construed as
|
||||
modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and may provide
|
||||
additional or different license terms and conditions for use, reproduction, or
|
||||
distribution of Your modifications, or for any such Derivative Works as a whole,
|
||||
provided Your use, reproduction, and distribution of the Work otherwise complies
|
||||
with the conditions stated in this License.
|
||||
|
||||
#### 5. Submission of Contributions
|
||||
|
||||
Unless You explicitly state otherwise, any Contribution intentionally submitted
|
||||
for inclusion in the Work by You to the Licensor shall be under the terms and
|
||||
conditions of this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify the terms of
|
||||
any separate license agreement you may have executed with Licensor regarding
|
||||
such Contributions.
|
||||
|
||||
#### 6. Trademarks
|
||||
|
||||
This License does not grant permission to use the trade names, trademarks,
|
||||
service marks, or product names of the Licensor, except as required for
|
||||
reasonable and customary use in describing the origin of the Work and
|
||||
reproducing the content of the NOTICE file.
|
||||
|
||||
#### 7. Disclaimer of Warranty
|
||||
|
||||
Unless required by applicable law or agreed to in writing, Licensor provides the
|
||||
Work (and each Contributor provides its Contributions) on an “AS IS” BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied,
|
||||
including, without limitation, any warranties or conditions of TITLE,
|
||||
NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are
|
||||
solely responsible for determining the appropriateness of using or
|
||||
redistributing the Work and assume any risks associated with Your exercise of
|
||||
permissions under this License.
|
||||
|
||||
#### 8. Limitation of Liability
|
||||
|
||||
In no event and under no legal theory, whether in tort (including negligence),
|
||||
contract, or otherwise, unless required by applicable law (such as deliberate
|
||||
and grossly negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special, incidental,
|
||||
or consequential damages of any character arising as a result of this License or
|
||||
out of the use or inability to use the Work (including but not limited to
|
||||
damages for loss of goodwill, work stoppage, computer failure or malfunction, or
|
||||
any and all other commercial damages or losses), even if such Contributor has
|
||||
been advised of the possibility of such damages.
|
||||
|
||||
#### 9. Accepting Warranty or Additional Liability
|
||||
|
||||
While redistributing the Work or Derivative Works thereof, You may choose to
|
||||
offer, and charge a fee for, acceptance of support, warranty, indemnity, or
|
||||
other liability obligations and/or rights consistent with this License. However,
|
||||
in accepting such obligations, You may act only on Your own behalf and on Your
|
||||
sole responsibility, not on behalf of any other Contributor, and only if You
|
||||
agree to indemnify, defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason of your
|
||||
accepting any such warranty or additional liability.
|
||||
|
||||
_END OF TERMS AND CONDITIONS_
|
||||
|
||||
### APPENDIX: How to apply the Apache License to your work
|
||||
|
||||
To apply the Apache License to your work, attach the following boilerplate
|
||||
notice, with the fields enclosed by brackets `[]` replaced with your own
|
||||
identifying information. (Don't include the brackets!) The text should be
|
||||
enclosed in the appropriate comment syntax for the file format. We also
|
||||
recommend that a file or class name and description of purpose be included on
|
||||
the same “printed page” as the copyright notice for easier identification within
|
||||
third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#pragma once
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
extern struct Module app_posix_module;
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,124 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#include <app/loader.h>
|
||||
#include <app/location.h>
|
||||
|
||||
#include <tactility/error.h>
|
||||
#include <tactility/log.h>
|
||||
|
||||
#include <service/manager.h>
|
||||
|
||||
#include <dlfcn.h>
|
||||
#include <sys/stat.h>
|
||||
|
||||
#include <new>
|
||||
#include <string>
|
||||
|
||||
constexpr auto* TAG = "app_posix_loader";
|
||||
|
||||
namespace {
|
||||
|
||||
/** load()-allocated state, passed back through run()/unload(). */
|
||||
struct PosixAppRuntime {
|
||||
void* handle = nullptr;
|
||||
};
|
||||
|
||||
bool is_regular_file(const std::string& path) {
|
||||
struct stat path_stat {};
|
||||
return ::stat(path.c_str(), &path_stat) == 0 && S_ISREG(path_stat.st_mode);
|
||||
}
|
||||
|
||||
// location.location can be either an app's install directory or the .so file directly; the
|
||||
// former resolves to the per-architecture binary at {dir}/elf/posix-{TACTILITY_POSIX_ARCH}.so,
|
||||
// mirroring app_esp32_loader_service.cpp's resolve_elf_path().
|
||||
error_t resolve_app_path(const std::string& path, std::string& resolvedPath) {
|
||||
if (path.ends_with(".so")) {
|
||||
resolvedPath = path;
|
||||
return ERROR_NONE;
|
||||
}
|
||||
std::string shared_object_path = path + "/elf/posix-" TACTILITY_POSIX_ARCH ".so";
|
||||
if (!is_regular_file(shared_object_path)) {
|
||||
return ERROR_NOT_FOUND;
|
||||
}
|
||||
resolvedPath = shared_object_path;
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
error_t api_load(AppLocation location, AppRuntime* out_runtime) {
|
||||
if (location.type != APP_LOCATION_PATH) {
|
||||
LOG_E(TAG, "Unsupported location type");
|
||||
return ERROR_NOT_SUPPORTED;
|
||||
}
|
||||
|
||||
std::string app_path;
|
||||
auto error = resolve_app_path(static_cast<const char*>(location.location), app_path);
|
||||
if (error != ERROR_NONE) {
|
||||
LOG_E(TAG, "Failed to resolve app path: %s", location.location);
|
||||
return error;
|
||||
}
|
||||
|
||||
LOG_I(TAG, "Loading %s", app_path.c_str());
|
||||
|
||||
// RTLD_NOW: a missing symbol fails here, not mid-run(). RTLD_LOCAL: this app's own exported
|
||||
// symbols (if any beyond its entry point) don't leak into the process's global scope and
|
||||
// clash with a different app's.
|
||||
void* handle = dlopen(app_path.c_str(), RTLD_NOW | RTLD_LOCAL);
|
||||
if (handle == nullptr) {
|
||||
LOG_E(TAG, "dlopen(%s) failed: %s", app_path.c_str(), dlerror());
|
||||
return ERROR_NOT_FOUND;
|
||||
}
|
||||
|
||||
auto* runtime = new (std::nothrow) PosixAppRuntime { .handle = handle };
|
||||
if (runtime == nullptr) {
|
||||
LOG_E(TAG, "Out of memory");
|
||||
dlclose(handle);
|
||||
return ERROR_OUT_OF_MEMORY;
|
||||
}
|
||||
|
||||
*out_runtime = runtime;
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
int32_t api_run(AppRuntime runtime_ptr, uint32_t /*app_instance_id*/, int argc, char* argv[]) {
|
||||
auto* runtime = static_cast<PosixAppRuntime*>(runtime_ptr);
|
||||
|
||||
dlerror(); // clear any pending error, per dlsym(3)'s own recommended idiom for telling a NULL
|
||||
// symbol address apart from a real lookup failure
|
||||
void* symbol = dlsym(runtime->handle, "main");
|
||||
const char* lookup_error = dlerror();
|
||||
if (symbol == nullptr || lookup_error != nullptr) {
|
||||
LOG_E(TAG, "dlsym(\"main\") failed: %s", lookup_error != nullptr ? lookup_error : "not found");
|
||||
return -1;
|
||||
}
|
||||
|
||||
auto* main_fn = reinterpret_cast<AppMainFn>(symbol);
|
||||
return main_fn(argc, argv);
|
||||
}
|
||||
|
||||
void api_unload(AppRuntime runtime_ptr) {
|
||||
auto* runtime = static_cast<PosixAppRuntime*>(runtime_ptr);
|
||||
dlclose(runtime->handle);
|
||||
delete runtime;
|
||||
}
|
||||
|
||||
AppLoaderApi loader_api = {
|
||||
.load = api_load,
|
||||
.run = api_run,
|
||||
.unload = api_unload,
|
||||
};
|
||||
|
||||
void* create_service(const ServiceManifest*) {
|
||||
return &loader_api;
|
||||
}
|
||||
|
||||
void destroy_service(const ServiceManifest*, void*) {
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
ServiceManifest loader_service_manifest = {
|
||||
.id = APP_LOADER_PATH_SERVICE_ID,
|
||||
.create_service = create_service,
|
||||
.destroy_service = destroy_service,
|
||||
.on_start = nullptr,
|
||||
.on_stop = nullptr,
|
||||
};
|
||||
@@ -0,0 +1,30 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#include <app_posix/module.h>
|
||||
|
||||
#include <service/manager.h>
|
||||
|
||||
#include <tactility/error.h>
|
||||
#include <tactility/module.h>
|
||||
|
||||
extern "C" {
|
||||
|
||||
extern ServiceManifest loader_service_manifest;
|
||||
|
||||
static error_t start() {
|
||||
return service_manager_add(&loader_service_manifest, /*auto_start=*/true);
|
||||
}
|
||||
|
||||
static error_t stop() {
|
||||
return service_manager_remove(loader_service_manifest.id);
|
||||
}
|
||||
|
||||
Module app_posix_module = {
|
||||
.name = "app-posix",
|
||||
.start = start,
|
||||
.stop = stop,
|
||||
.drivers = nullptr,
|
||||
.symbols = nullptr,
|
||||
.internal = nullptr,
|
||||
};
|
||||
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
project(AppPosixModuleTests)
|
||||
|
||||
enable_language(C CXX ASM)
|
||||
|
||||
# Fixture: a tiny shared object with a main() the test dlopen()s through app-posix-module's real
|
||||
# loader service. Deliberately not linked against app-module, so its call into
|
||||
# app_scheduler_current_app_id() stays undefined until dlopen() resolves it against
|
||||
# AppPosixModuleTests's own copy, exactly like a real Tactility-embedded app would resolve
|
||||
# against the running Tactility binary.
|
||||
add_library(app_posix_module_test_fixture SHARED EXCLUDE_FROM_ALL ${CMAKE_CURRENT_LIST_DIR}/fixtures/fixture_app.cpp)
|
||||
target_include_directories(app_posix_module_test_fixture PRIVATE ${CMAKE_SOURCE_DIR}/Modules/app-module/include)
|
||||
set_target_properties(app_posix_module_test_fixture PROPERTIES POSITION_INDEPENDENT_CODE ON)
|
||||
|
||||
file(GLOB_RECURSE TEST_SOURCES CONFIGURE_DEPENDS ${PROJECT_SOURCE_DIR}/source/*.cpp)
|
||||
add_executable(AppPosixModuleTests EXCLUDE_FROM_ALL ${TEST_SOURCES})
|
||||
add_dependencies(AppPosixModuleTests app_posix_module_test_fixture)
|
||||
|
||||
target_include_directories(AppPosixModuleTests PRIVATE ${DOCTESTINC})
|
||||
target_compile_definitions(AppPosixModuleTests PRIVATE
|
||||
FIXTURE_APP_PATH="$<TARGET_FILE:app_posix_module_test_fixture>"
|
||||
)
|
||||
|
||||
add_test(NAME AppPosixModuleTests COMMAND AppPosixModuleTests)
|
||||
|
||||
target_link_libraries(AppPosixModuleTests PUBLIC
|
||||
TactilityKernel
|
||||
app-module
|
||||
app-posix-module
|
||||
service-module
|
||||
platform-posix
|
||||
freertos_kernel
|
||||
${CMAKE_DL_LIBS}
|
||||
)
|
||||
# So the fixture's undefined app_scheduler_current_app_id() reference can resolve against this
|
||||
# test binary's own copy at dlopen() time. See app-posix-module's own ENABLE_EXPORTS comment on
|
||||
# the real Tactility executable for why this is needed.
|
||||
set_target_properties(AppPosixModuleTests PROPERTIES ENABLE_EXPORTS ON)
|
||||
@@ -0,0 +1,12 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#include <app/scheduler.h>
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
// Deliberately not linked against app-module: app_scheduler_current_app_id() stays an undefined
|
||||
// symbol in this .so, resolved at dlopen() time against the loading process's own copy. Proves
|
||||
// app-posix-module's loader lets a loaded app call straight back into Tactility without linking
|
||||
// its own copy of it.
|
||||
extern "C" int32_t main(int, char*[]) {
|
||||
return static_cast<int32_t>(app_scheduler_current_app_id());
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#include "doctest.h"
|
||||
|
||||
#include <app/event.h>
|
||||
#include <app/loader.h>
|
||||
#include <app/manager.h>
|
||||
#include <app/scheduler.h>
|
||||
|
||||
#include <service/manager.h>
|
||||
|
||||
#include <tactility/delay.h>
|
||||
|
||||
#include <atomic>
|
||||
|
||||
extern ServiceManifest loader_service_manifest; // app-posix-module's own
|
||||
extern ServiceManifest app_internal_loader_service_manifest; // app-module's real memory loader
|
||||
|
||||
namespace {
|
||||
|
||||
void ensure_path_loader_registered() {
|
||||
if (service_manager_find_instance(APP_LOADER_PATH_SERVICE_ID) == nullptr) {
|
||||
service_manager_add(&loader_service_manifest, /*auto_start=*/true);
|
||||
}
|
||||
}
|
||||
|
||||
void ensure_memory_loader_registered() {
|
||||
if (service_manager_find_instance(APP_LOADER_MEMORY_SERVICE_ID) == nullptr) {
|
||||
service_manager_add(&app_internal_loader_service_manifest, /*auto_start=*/true);
|
||||
}
|
||||
}
|
||||
|
||||
bool wait_for_state(AppInstanceId id, AppInstanceState target, uint32_t timeout_ms) {
|
||||
uint32_t waited = 0;
|
||||
while (waited < timeout_ms) {
|
||||
if (app_manager_get_state(id) == target) {
|
||||
return true;
|
||||
}
|
||||
delay_millis(10);
|
||||
waited += 10;
|
||||
}
|
||||
return app_manager_get_state(id) == target;
|
||||
}
|
||||
|
||||
std::atomic<int32_t> g_fixture_result { -1 };
|
||||
std::atomic<bool> g_fixture_result_received { false };
|
||||
|
||||
// Starts the dlopen()ed fixture as its own modal child and stashes its returned result, so the
|
||||
// test can inspect that result from the (in-process, directly readable) parent's own task.
|
||||
int32_t parent_app_main(int, char*[]) {
|
||||
TaskEventGroup event_group {};
|
||||
task_event_group_construct(&event_group);
|
||||
|
||||
AppEventSubscription sub {};
|
||||
app_event_subscribe(&sub, &event_group);
|
||||
|
||||
AppInstanceId self_id = app_scheduler_current_app_id();
|
||||
|
||||
AppManifest fixture_manifest {
|
||||
"test.posix.fixture", "Fixture", APP_CATEGORY_USER,
|
||||
{ APP_LOCATION_PATH, const_cast<char*>(FIXTURE_APP_PATH) }
|
||||
};
|
||||
app_manager_add(&fixture_manifest);
|
||||
|
||||
AppInstanceId fixture_id = 0;
|
||||
app_manager_start_for_result("test.posix.fixture", self_id, 0, nullptr, &fixture_id);
|
||||
|
||||
while (true) {
|
||||
if (task_event_group_wait_any(&event_group, nullptr, pdMS_TO_TICKS(5000)) != ERROR_NONE) {
|
||||
break; // safety net so a bug here can't hang the test suite
|
||||
}
|
||||
AppEvent event {};
|
||||
bool got_result = false;
|
||||
while (app_event_poll(&sub, &event) == ERROR_NONE) {
|
||||
if (event.type == APP_EVENT_RESULT && event.result.launch_id == fixture_id) {
|
||||
g_fixture_result.store(event.result.result, std::memory_order_release);
|
||||
got_result = true;
|
||||
}
|
||||
}
|
||||
if (got_result) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
g_fixture_result_received.store(true, std::memory_order_release);
|
||||
|
||||
app_manager_remove("test.posix.fixture");
|
||||
app_event_unsubscribe(&sub);
|
||||
task_event_group_destruct(&event_group);
|
||||
return 0;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST_CASE("app-posix-module's loader-path service dlopen()s a .so and calls its main(), which resolves a real Tactility symbol against the host") {
|
||||
ensure_path_loader_registered();
|
||||
ensure_memory_loader_registered();
|
||||
g_fixture_result.store(-1, std::memory_order_relaxed);
|
||||
g_fixture_result_received.store(false, std::memory_order_relaxed);
|
||||
|
||||
AppManifest parent_manifest { "test.posix.parent", "Parent", APP_CATEGORY_USER, { APP_LOCATION_MEMORY, reinterpret_cast<void*>(parent_app_main) } };
|
||||
REQUIRE_EQ(app_manager_add(&parent_manifest), ERROR_NONE);
|
||||
|
||||
AppInstanceId parent_id = 0;
|
||||
REQUIRE_EQ(app_manager_start("test.posix.parent", &parent_id), ERROR_NONE);
|
||||
REQUIRE(wait_for_state(parent_id, APP_INSTANCE_STATE_STOPPED, 3000));
|
||||
|
||||
CHECK(g_fixture_result_received.load(std::memory_order_acquire));
|
||||
// A positive AppInstanceId proves the fixture's dlopen()ed main() actually resolved and
|
||||
// called app_scheduler_current_app_id() against the host process, not just "ran and returned
|
||||
// a hardcoded value" - 0 would mean it thought it wasn't running as an app instance at all.
|
||||
CHECK_GT(g_fixture_result.load(std::memory_order_acquire), 0);
|
||||
|
||||
app_manager_remove("test.posix.parent");
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
#define DOCTEST_CONFIG_IMPLEMENT
|
||||
#include "doctest.h"
|
||||
#include <cassert>
|
||||
|
||||
#include "FreeRTOS.h"
|
||||
#include "task.h"
|
||||
|
||||
typedef struct {
|
||||
int argc;
|
||||
char** argv;
|
||||
int result;
|
||||
} TestTaskData;
|
||||
|
||||
void test_task(void* parameter) {
|
||||
auto* data = (TestTaskData*)parameter;
|
||||
|
||||
doctest::Context context;
|
||||
|
||||
context.applyCommandLine(data->argc, data->argv);
|
||||
|
||||
// overrides
|
||||
context.setOption("no-breaks", true); // don't break in the debugger when assertions fail
|
||||
|
||||
data->result = context.run();
|
||||
|
||||
vTaskEndScheduler();
|
||||
|
||||
vTaskDelete(nullptr);
|
||||
}
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
TestTaskData data = {
|
||||
.argc = argc,
|
||||
.argv = argv,
|
||||
.result = 0
|
||||
};
|
||||
|
||||
BaseType_t task_result = xTaskCreate(
|
||||
test_task,
|
||||
"test_task",
|
||||
8192,
|
||||
&data,
|
||||
1,
|
||||
nullptr
|
||||
);
|
||||
|
||||
if (task_result != pdPASS) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
vTaskStartScheduler();
|
||||
|
||||
return data.result;
|
||||
}
|
||||
@@ -28,6 +28,11 @@ static uint32_t task_max_sleep_ms = 10;
|
||||
static TaskHandle_t lvgl_task_handle = NULL;
|
||||
static bool lvgl_task_interrupt_requested = false;
|
||||
|
||||
// ESP32 gets LVGL's tick driven for free by esp_lvgl_port; POSIX has no such
|
||||
// helper, so lv_tick_get() would otherwise stay at 0 forever and every timer
|
||||
// (indev polling included) would look permanently "not due yet".
|
||||
static uint32_t lvgl_last_tick_millis = 0;
|
||||
|
||||
#define LVGL_STOP_POLL_INTERVAL 10
|
||||
#define LVGL_STOP_TIMEOUT 5000
|
||||
|
||||
@@ -80,7 +85,13 @@ static void lvgl_task(void* arg) {
|
||||
// on_start must be called from the task, otherwise the display doesn't work
|
||||
if (lvgl_module_config.on_start) lvgl_module_config.on_start();
|
||||
|
||||
lvgl_last_tick_millis = (uint32_t)get_millis();
|
||||
|
||||
while (!lvgl_task_is_interrupt_requested()) {
|
||||
uint32_t now_millis = (uint32_t)get_millis();
|
||||
lv_tick_inc(now_millis - lvgl_last_tick_millis);
|
||||
lvgl_last_tick_millis = now_millis;
|
||||
|
||||
if (lvgl_try_lock(10)) {
|
||||
task_delay_ms = lv_timer_handler();
|
||||
lvgl_unlock();
|
||||
|
||||
@@ -8,6 +8,7 @@ extern "C" {
|
||||
|
||||
static const ModuleSymbol SYMBOLS[] = {
|
||||
DEFINE_MODULE_SYMBOL(window_manager_create),
|
||||
DEFINE_MODULE_SYMBOL(window_manager_create_ext),
|
||||
DEFINE_MODULE_SYMBOL(window_manager_remove),
|
||||
DEFINE_MODULE_SYMBOL(window_manager_get_state),
|
||||
DEFINE_MODULE_SYMBOL(window_manager_await_state_change),
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
#include <pthread.h>
|
||||
#include <stddef.h>
|
||||
|
||||
/**
|
||||
* Linked in with -Wl,--wrap=pthread_attr_setstack (see Tactility/CMakeLists.txt).
|
||||
*
|
||||
* FreeRTOS's POSIX port (Libraries/FreeRTOS-Kernel/portable/ThirdParty/GCC/Posix/port.c)
|
||||
* hands every task a stack carved out of its own heap (pvPortMalloc) via this call.
|
||||
* pthread_attr_setstack requires page alignment, which pvPortMalloc doesn't guarantee;
|
||||
* when it happens to succeed anyway (allocator alignment can vary run to run), the
|
||||
* task's real pthread stack ends up living inside that small FreeRTOS heap region -
|
||||
* fine for typical embedded task code, but a desktop GL driver doing on-the-fly shader
|
||||
* compilation on that thread (e.g. Mesa on first frame present) can overflow it and
|
||||
* silently corrupt adjacent heap_4 objects.
|
||||
*
|
||||
* Wrapping the call out entirely (rather than patching the vendored port.c) leaves every
|
||||
* task's pthread_attr_t at its pthread_attr_init() default, so pthread_create() always
|
||||
* gives it a real, properly allocated default-size stack instead.
|
||||
*/
|
||||
int __wrap_pthread_attr_setstack(pthread_attr_t* attr, void* stackaddr, size_t stacksize) {
|
||||
(void)attr;
|
||||
(void)stackaddr;
|
||||
(void)stacksize;
|
||||
return 0;
|
||||
}
|
||||
@@ -88,6 +88,7 @@ else ()
|
||||
|
||||
list(APPEND REQUIRES_LIST
|
||||
platform-posix
|
||||
app-posix-module
|
||||
freertos_kernel
|
||||
cJSON
|
||||
lvgl
|
||||
@@ -181,6 +182,34 @@ else ()
|
||||
target_include_directories(Tactility PUBLIC Include/)
|
||||
target_include_directories(Tactility PRIVATE Private/)
|
||||
target_link_libraries(Tactility PRIVATE ${TACTILITY_REQUIRES_LIST})
|
||||
# Exports Tactility's own symbols (-rdynamic) so a dlopen()ed app-posix-module app can resolve
|
||||
# calls back into it - the OS-native equivalent of app-esp32-module's custom symbol resolver.
|
||||
set_target_properties(Tactility PROPERTIES ENABLE_EXPORTS ON)
|
||||
# Routes every pthread_attr_setstack() call (FreeRTOS's POSIX port hands each task a stack
|
||||
# carved out of its own heap through this) to __wrap_pthread_attr_setstack() in
|
||||
# Platforms/platform-posix/source/pthread_stack_wrap.c, which no-ops it - see that file for why.
|
||||
# --wrap is a GNU ld option; Apple's linker doesn't support it.
|
||||
if (NOT APPLE)
|
||||
target_link_options(Tactility PRIVATE "-Wl,--wrap=pthread_attr_setstack")
|
||||
# Routes every read()/write()/close() call to Tactility/Source/AppStdioWrap.cpp's
|
||||
# __wrap_read/write/close(), which forward into app_io_read/write/close() - the fd-table
|
||||
# dispatch that lets an app's own stdio (e.g. a fileselection dialog's printf'd result
|
||||
# path) reach an AppStream a parent bound via app_manager_start_with_streams(). Mirrors
|
||||
# what ESP-IDF's build already does for the ESP32 target (see top-level CMakeLists.txt).
|
||||
target_link_options(Tactility PRIVATE "-Wl,--wrap=read" "-Wl,--wrap=write" "-Wl,--wrap=close")
|
||||
# glibc's printf/fprintf/etc don't call the public write() symbol internally (they're
|
||||
# already compiled into libc.so, out of --wrap's reach), so the read/write/close wrap
|
||||
# above can't see them. Newlib (ESP-IDF) doesn't have this gap - its stdio does call the
|
||||
# wrappable syscall stubs - so this block is POSIX-only. Wrapping these symbols instead
|
||||
# redirects OUR OWN calls to them (the only ones --wrap can rewrite) through
|
||||
# AppStdioWrap.cpp's __wrap_* functions, which check the target stream (stdout/stdin) and
|
||||
# fall back to the real libc function for any other FILE*.
|
||||
target_link_options(Tactility PRIVATE
|
||||
"-Wl,--wrap=printf" "-Wl,--wrap=fprintf" "-Wl,--wrap=vprintf" "-Wl,--wrap=vfprintf"
|
||||
"-Wl,--wrap=puts" "-Wl,--wrap=fputs" "-Wl,--wrap=putchar" "-Wl,--wrap=fputc"
|
||||
"-Wl,--wrap=getchar" "-Wl,--wrap=fgetc" "-Wl,--wrap=fgets"
|
||||
)
|
||||
endif ()
|
||||
endif ()
|
||||
|
||||
#
|
||||
|
||||
@@ -1,29 +1,36 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
|
||||
#include <app/stream.h>
|
||||
#include <tactility/concurrent/task_event_group.h>
|
||||
|
||||
namespace tt::app::fileselection {
|
||||
|
||||
/**
|
||||
* Show a file selection dialog that allows the user to select an existing file, as a modal
|
||||
* child of @a callerAppInstanceId (see app_manager_start_for_result()). Result (0 = Ok,
|
||||
* 1 = Cancelled) is delivered back via APP_EVENT_RESULT once this app's thread exits - call
|
||||
* getLastPath() right after receiving it, on result == 0. The caller must call
|
||||
* app_manager_stop() on the returned instance id once that event arrives, to fully reap this
|
||||
* instance.
|
||||
* child of @a callerAppInstanceId (see app_manager_start_for_result_with_streams()). Result
|
||||
* (0 = Ok, 1 = Cancelled) is delivered back via APP_EVENT_RESULT once this app's thread exits.
|
||||
* On result == 0, read the picked path with app_stream_read(&stream, ...) then
|
||||
* app_stream_unsubscribe(&stream); on any other result, just app_stream_unsubscribe(&stream).
|
||||
* The caller must call app_manager_stop() on the returned instance id once that event arrives,
|
||||
* to fully reap this instance.
|
||||
* @param[in,out] stream caller-owned storage bound to the started app's stdout; must stay valid
|
||||
* until app_stream_unsubscribe() is called on it (see above).
|
||||
* @param[in] buffer caller-owned backing storage for @a stream's ring buffer; must stay valid
|
||||
* for the same duration as @a stream.
|
||||
* @param[in] bufferCapacity size of @a buffer in bytes.
|
||||
* @param[in] eventGroup the caller's own event group, reused for the stream's readiness bits
|
||||
* (see app_stream_subscribe()) - the caller isn't required to actually wait on them itself.
|
||||
* @return the new app instance id
|
||||
*/
|
||||
uint32_t startForExistingFile(uint32_t callerAppInstanceId);
|
||||
uint32_t startForExistingFile(uint32_t callerAppInstanceId, AppStream& stream, void* buffer, size_t bufferCapacity, TaskEventGroup* eventGroup);
|
||||
|
||||
/**
|
||||
* Same as startForExistingFile(), but also allows picking a path that doesn't exist yet (for
|
||||
* "save as"-style flows).
|
||||
*/
|
||||
uint32_t startForExistingOrNewFile(uint32_t callerAppInstanceId);
|
||||
|
||||
/** @return the path picked by the last FileSelection dialog that closed with result == Ok. Only
|
||||
* one dialog is expected to be open at a time. */
|
||||
std::string getLastPath();
|
||||
uint32_t startForExistingOrNewFile(uint32_t callerAppInstanceId, AppStream& stream, void* buffer, size_t bufferCapacity, TaskEventGroup* eventGroup);
|
||||
|
||||
} // namespace
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
#pragma once
|
||||
|
||||
#ifndef ESP_PLATFORM
|
||||
|
||||
namespace tt {
|
||||
|
||||
bool initPartitionsPosix();
|
||||
|
||||
} // namespace
|
||||
|
||||
#endif // ESP_PLATFORM
|
||||
@@ -1,6 +1,9 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#ifdef ESP_PLATFORM
|
||||
|
||||
// Paired with -Wl,--wrap=read/write/close - see Tactility/CMakeLists.txt (POSIX) and the
|
||||
// top-level CMakeLists.txt (ESP32) for where that's applied. On a platform where it isn't
|
||||
// (currently: macOS, whose linker doesn't support --wrap), these are simply never called - real
|
||||
// read()/write()/close() calls go straight through unredirected.
|
||||
#include <app/io.h>
|
||||
|
||||
#include <sys/types.h>
|
||||
@@ -21,4 +24,177 @@ int __wrap_close(int fd) {
|
||||
|
||||
}
|
||||
|
||||
#endif // ESP_PLATFORM
|
||||
// region glibc stdio wraps
|
||||
//
|
||||
// glibc's printf/fprintf/etc are compiled into libc.so and call an internal, non-exported write()
|
||||
// alias - --wrap=write (above) can't reach that call, only calls WE make to the public symbol.
|
||||
// These wraps instead redirect calls WE make to printf/fprintf/etc, the same trick as read/write/
|
||||
// close above. Newlib (ESP-IDF) doesn't have this gap - its stdio does call the wrappable syscall
|
||||
// stubs - so Tactility/CMakeLists.txt only applies the matching -Wl,--wrap= flags on POSIX.
|
||||
//
|
||||
// Scoped to the printf/getc families only: fread/fwrite take an arbitrary FILE* and are already
|
||||
// used sitewide for real file I/O (e.g. File.cpp's readBinaryInternal), so wrapping them would
|
||||
// route every such call through this file's stdin/stdout check - a correctness risk for unrelated
|
||||
// code that isn't worth taking here. putc/getc are excluded too since glibc defines them as
|
||||
// macros, not real calls, so wrapping those symbols wouldn't reliably intercept them.
|
||||
|
||||
#if !defined(ESP_PLATFORM) && !defined(__APPLE__)
|
||||
|
||||
#include <cstdarg>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <memory>
|
||||
#include <unistd.h>
|
||||
|
||||
extern "C" {
|
||||
int __real_vfprintf(FILE* stream, const char* format, va_list args);
|
||||
int __real_fputs(const char* s, FILE* stream);
|
||||
int __real_fputc(int c, FILE* stream);
|
||||
int __real_fgetc(FILE* stream);
|
||||
char* __real_fgets(char* buffer, int size, FILE* stream);
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
void writeAllToStdout(const void* data, size_t size) {
|
||||
const auto* bytes = static_cast<const char*>(data);
|
||||
size_t remaining = size;
|
||||
while (remaining > 0) {
|
||||
ssize_t written = app_io_write(STDOUT_FILENO, bytes, remaining);
|
||||
if (written <= 0) {
|
||||
break;
|
||||
}
|
||||
bytes += written;
|
||||
remaining -= static_cast<size_t>(written);
|
||||
}
|
||||
}
|
||||
|
||||
// Formats into stdout via app_io_write() rather than through a FILE*'s own buffering, since that
|
||||
// buffering is exactly what glibc's internal write() call sidesteps --wrap for in the first place.
|
||||
int formatToStdout(const char* format, va_list args) {
|
||||
char stackBuffer[256];
|
||||
va_list argsForStack;
|
||||
va_copy(argsForStack, args);
|
||||
int needed = vsnprintf(stackBuffer, sizeof(stackBuffer), format, argsForStack);
|
||||
va_end(argsForStack);
|
||||
if (needed < 0) {
|
||||
return needed;
|
||||
}
|
||||
if (static_cast<size_t>(needed) < sizeof(stackBuffer)) {
|
||||
writeAllToStdout(stackBuffer, static_cast<size_t>(needed));
|
||||
return needed;
|
||||
}
|
||||
auto heapBuffer = std::make_unique<char[]>(static_cast<size_t>(needed) + 1);
|
||||
va_list argsForHeap;
|
||||
va_copy(argsForHeap, args);
|
||||
vsnprintf(heapBuffer.get(), static_cast<size_t>(needed) + 1, format, argsForHeap);
|
||||
va_end(argsForHeap);
|
||||
writeAllToStdout(heapBuffer.get(), static_cast<size_t>(needed));
|
||||
return needed;
|
||||
}
|
||||
|
||||
int readOneFromStdin(char& out) {
|
||||
return static_cast<int>(app_io_read(STDIN_FILENO, &out, 1));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
extern "C" {
|
||||
|
||||
int __wrap_vprintf(const char* format, va_list args) {
|
||||
return formatToStdout(format, args);
|
||||
}
|
||||
|
||||
int __wrap_printf(const char* format, ...) {
|
||||
va_list args;
|
||||
va_start(args, format);
|
||||
int result = formatToStdout(format, args);
|
||||
va_end(args);
|
||||
return result;
|
||||
}
|
||||
|
||||
int __wrap_vfprintf(FILE* stream, const char* format, va_list args) {
|
||||
if (stream == stdout) {
|
||||
return formatToStdout(format, args);
|
||||
}
|
||||
return __real_vfprintf(stream, format, args);
|
||||
}
|
||||
|
||||
int __wrap_fprintf(FILE* stream, const char* format, ...) {
|
||||
va_list args;
|
||||
va_start(args, format);
|
||||
int result = (stream == stdout) ? formatToStdout(format, args) : __real_vfprintf(stream, format, args);
|
||||
va_end(args);
|
||||
return result;
|
||||
}
|
||||
|
||||
int __wrap_puts(const char* s) {
|
||||
writeAllToStdout(s, strlen(s));
|
||||
writeAllToStdout("\n", 1);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int __wrap_fputs(const char* s, FILE* stream) {
|
||||
if (stream == stdout) {
|
||||
writeAllToStdout(s, strlen(s));
|
||||
return 0;
|
||||
}
|
||||
return __real_fputs(s, stream);
|
||||
}
|
||||
|
||||
int __wrap_putchar(int c) {
|
||||
auto ch = static_cast<char>(c);
|
||||
writeAllToStdout(&ch, 1);
|
||||
return c;
|
||||
}
|
||||
|
||||
int __wrap_fputc(int c, FILE* stream) {
|
||||
if (stream == stdout) {
|
||||
return __wrap_putchar(c);
|
||||
}
|
||||
return __real_fputc(c, stream);
|
||||
}
|
||||
|
||||
int __wrap_getchar() {
|
||||
char c;
|
||||
return readOneFromStdin(c) == 1 ? static_cast<unsigned char>(c) : EOF;
|
||||
}
|
||||
|
||||
int __wrap_fgetc(FILE* stream) {
|
||||
if (stream == stdin) {
|
||||
return __wrap_getchar();
|
||||
}
|
||||
return __real_fgetc(stream);
|
||||
}
|
||||
|
||||
char* __wrap_fgets(char* buffer, int size, FILE* stream) {
|
||||
if (stream != stdin) {
|
||||
return __real_fgets(buffer, size, stream);
|
||||
}
|
||||
if (size <= 0) {
|
||||
return nullptr;
|
||||
}
|
||||
int i = 0;
|
||||
for (; i < size - 1; ++i) {
|
||||
char c;
|
||||
if (readOneFromStdin(c) != 1) {
|
||||
break;
|
||||
}
|
||||
buffer[i] = c;
|
||||
if (c == '\n') {
|
||||
++i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (i == 0) {
|
||||
return nullptr;
|
||||
}
|
||||
buffer[i] = '\0';
|
||||
return buffer;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#endif // !ESP_PLATFORM && !__APPLE__
|
||||
|
||||
// endregion
|
||||
|
||||
@@ -52,11 +52,7 @@ std::string getUserDataRootPath() {
|
||||
}
|
||||
|
||||
std::string getDataPath() {
|
||||
#ifdef ESP_PLATFORM
|
||||
return getUserDataRootPath() + "/tactility";
|
||||
#else
|
||||
return "data";
|
||||
#endif
|
||||
}
|
||||
|
||||
std::string getTempPath() {
|
||||
|
||||
@@ -18,7 +18,11 @@ std::vector<dirent> getFileSystemDirents() {
|
||||
if (!file_system_is_mounted(fs)) return true;
|
||||
char path[128];
|
||||
if (file_system_get_path(fs, path, sizeof(path)) != ERROR_NONE) return true;
|
||||
auto mount_name = std::string(path).substr(1);
|
||||
// ESP32 mount paths are short names ("/system"); POSIX file systems can return a full
|
||||
// absolute host path instead, so take the last path component either way.
|
||||
auto path_str = std::string(path);
|
||||
auto slash_pos = path_str.find_last_of('/');
|
||||
auto mount_name = slash_pos == std::string::npos ? path_str : path_str.substr(slash_pos + 1);
|
||||
if (!config::SHOW_SYSTEM_PARTITION && mount_name.starts_with(SYSTEM_PARTITION_NAME)) return true;
|
||||
auto dir_entry = dirent {
|
||||
.d_ino = 2,
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
#ifndef ESP_PLATFORM
|
||||
|
||||
#include <Tactility/PartitionsPosix.h>
|
||||
#include <Tactility/MountPoints.h>
|
||||
|
||||
#include <tactility/error.h>
|
||||
#include <tactility/filesystem/file_system.h>
|
||||
#include <tactility/log.h>
|
||||
|
||||
#include <cerrno>
|
||||
#include <climits>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
|
||||
namespace tt {
|
||||
|
||||
constexpr auto* TAG = "Partitions";
|
||||
|
||||
// region file_system stub
|
||||
|
||||
// A plain host directory has no real mount/unmount step to perform - "mounted" here just tracks
|
||||
// whether file_system_remove()'s precondition (must be unmounted first) has been satisfied.
|
||||
struct DirectoryFsData {
|
||||
char path[PATH_MAX];
|
||||
bool mounted;
|
||||
};
|
||||
|
||||
static DirectoryFsData system_fs_data;
|
||||
static DirectoryFsData data_fs_data;
|
||||
static FileSystem* system_fs = nullptr;
|
||||
static FileSystem* data_fs = nullptr;
|
||||
|
||||
static error_t mount(void* data) {
|
||||
static_cast<DirectoryFsData*>(data)->mounted = true;
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
static error_t unmount(void* data) {
|
||||
static_cast<DirectoryFsData*>(data)->mounted = false;
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
static bool is_mounted(void* data) {
|
||||
return static_cast<DirectoryFsData*>(data)->mounted;
|
||||
}
|
||||
|
||||
static error_t get_path(void* data, char* out_path, size_t out_path_size) {
|
||||
auto* fs_data = static_cast<DirectoryFsData*>(data);
|
||||
if (strlen(fs_data->path) >= out_path_size) {
|
||||
return ERROR_BUFFER_OVERFLOW;
|
||||
}
|
||||
strcpy(out_path, fs_data->path);
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
static const FileSystemApi directory_fs_api = {
|
||||
.mount = mount,
|
||||
.unmount = unmount,
|
||||
.is_mounted = is_mounted,
|
||||
.get_path = get_path,
|
||||
};
|
||||
|
||||
// endregion file_system stub
|
||||
|
||||
// relative_path is resolved against the process' current working directory (the simulator is
|
||||
// expected to run with Data/ as its working directory, so file::SYSTEM_PARTITION_NAME/
|
||||
// DATA_PARTITION_NAME here match file::MOUNT_POINT_SYSTEM/MOUNT_POINT_DATA).
|
||||
static FileSystem* registerDirectoryFs(const char* relativePath, DirectoryFsData* outData) {
|
||||
if (realpath(relativePath, outData->path) == nullptr) {
|
||||
LOG_E(TAG, "Failed to resolve '%s' to an absolute path: %s", relativePath, strerror(errno));
|
||||
return nullptr;
|
||||
}
|
||||
outData->mounted = true;
|
||||
return file_system_add(&directory_fs_api, outData);
|
||||
}
|
||||
|
||||
static void unregisterDirectoryFs(FileSystem* fs) {
|
||||
if (fs == nullptr) {
|
||||
return;
|
||||
}
|
||||
file_system_unmount(fs);
|
||||
file_system_remove(fs);
|
||||
}
|
||||
|
||||
bool initPartitionsPosix() {
|
||||
system_fs = registerDirectoryFs(file::SYSTEM_PARTITION_NAME, &system_fs_data);
|
||||
if (system_fs == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
data_fs = registerDirectoryFs(file::DATA_PARTITION_NAME, &data_fs_data);
|
||||
if (data_fs == nullptr) {
|
||||
unregisterDirectoryFs(system_fs);
|
||||
system_fs = nullptr;
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
#endif // ESP_PLATFORM
|
||||
@@ -4,13 +4,27 @@
|
||||
#include <app_esp32/module.h>
|
||||
#endif
|
||||
|
||||
#if __has_include(<unistd.h>) && not defined(ESP_PLATFORM)
|
||||
#define TT_IS_POSIX 1
|
||||
#else
|
||||
#define TT_IS_POSIX 0
|
||||
#endif
|
||||
|
||||
#if TT_IS_POSIX or defined(ESP_PLATFORM) // esp-idf supports certain posix symbols
|
||||
#include <posix_symbols/module.h>
|
||||
#endif
|
||||
|
||||
#if TT_IS_POSIX
|
||||
#include <app_posix/module.h>
|
||||
#include <Tactility/PartitionsPosix.h>
|
||||
#endif
|
||||
|
||||
#include <format>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include <app/event.h>
|
||||
#include <app/install.h>
|
||||
#include <app/manager.h>
|
||||
#include <app/manifest.h>
|
||||
#include <app/module.h>
|
||||
@@ -19,7 +33,6 @@
|
||||
|
||||
#include <Tactility/CpuAffinity.h>
|
||||
#include <Tactility/DeprecatedPaths.h>
|
||||
#include <Tactility/LogMessages.h>
|
||||
#include <Tactility/MountPoints.h>
|
||||
#include <Tactility/TactilityConfig.h>
|
||||
#include <Tactility/bluetooth/Bluetooth.h>
|
||||
@@ -35,7 +48,10 @@
|
||||
#include <Tactility/service/audio/Audio.h>
|
||||
#include <Tactility/settings/DisplaySettings.h>
|
||||
#include <Tactility/settings/TimePrivate.h>
|
||||
|
||||
#ifdef CONFIG_TT_TOUCH_CALIBRATION_SUPPORTED
|
||||
#include <Tactility/settings/TouchCalibrationSettings.h>
|
||||
#endif
|
||||
|
||||
#include <c_symbols/module.h>
|
||||
#include <cpp_symbols/module.h>
|
||||
@@ -47,7 +63,6 @@
|
||||
#include <gps_meshtastic/module.h>
|
||||
#include <http/module.h>
|
||||
#include <mbedtls/module.h>
|
||||
#include <posix_symbols/module.h>
|
||||
#include <pthread/module.h>
|
||||
|
||||
#include <crypt/module.h>
|
||||
@@ -501,7 +516,9 @@ void run(Module* const dtsModules[], const DtsDevice dtsDevices[]) {
|
||||
|
||||
// C/C++/Posix symbols
|
||||
check(module_ensure_started(&c_symbols_module) == ERROR_NONE);
|
||||
#if TT_IS_POSIX or defined(ESP_PLATFORM) // esp-idf supports certain posix symbols
|
||||
check(module_ensure_started(&posix_symbols_module) == ERROR_NONE);
|
||||
#endif
|
||||
check(module_ensure_started(&cpp_symbols_module) == ERROR_NONE);
|
||||
// OS level symbols
|
||||
check(module_ensure_started(&freertos_module) == ERROR_NONE);
|
||||
@@ -516,10 +533,14 @@ void run(Module* const dtsModules[], const DtsDevice dtsDevices[]) {
|
||||
check(module_ensure_started(&gps_meshtastic_module) == ERROR_NONE);
|
||||
#ifdef ESP_PLATFORM
|
||||
check(module_ensure_started(&app_esp32_module) == ERROR_NONE);
|
||||
#elif TT_IS_POSIX
|
||||
check(module_ensure_started(&app_posix_module) == ERROR_NONE);
|
||||
#endif
|
||||
|
||||
#ifdef ESP_PLATFORM
|
||||
initEsp();
|
||||
#elif TT_IS_POSIX
|
||||
check(initPartitionsPosix(), "Failed to init partitions");
|
||||
#endif
|
||||
|
||||
settings::initTimeZone();
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <fcntl.h>
|
||||
#include <format>
|
||||
#include <unistd.h>
|
||||
|
||||
namespace tt::app::files {
|
||||
@@ -174,43 +175,22 @@ static bool copyRecursive(const std::string& src, const std::string& dst) {
|
||||
|
||||
void View::viewFile(const std::string& path, const std::string& filename) {
|
||||
std::string file_path = path + "/" + filename;
|
||||
|
||||
// For PC we need to make the path relative to the current work directory,
|
||||
// because that's how LVGL maps its 'drive letter' to the file system.
|
||||
std::string processed_filepath;
|
||||
if (kernel::getPlatform() == kernel::PlatformSimulator) {
|
||||
char cwd[PATH_MAX];
|
||||
if (getcwd(cwd, sizeof(cwd)) == nullptr) {
|
||||
LOG_E(TAG, "Failed to get current working directory");
|
||||
return;
|
||||
}
|
||||
if (!file_path.starts_with(cwd)) {
|
||||
LOG_E(TAG, "Can only work with files in working directory %s", cwd);
|
||||
return;
|
||||
}
|
||||
processed_filepath = file_path.substr(strlen(cwd));
|
||||
} else {
|
||||
processed_filepath = file_path;
|
||||
}
|
||||
|
||||
LOG_I(TAG, "Clicked %s", file_path.c_str());
|
||||
|
||||
if (isSupportedAppFile(filename)) {
|
||||
#ifdef ESP_PLATFORM
|
||||
// install(filename);
|
||||
auto message = std::format("Do you want to install {}?", filename);
|
||||
installAppPath = processed_filepath;
|
||||
installAppPath = file_path;
|
||||
auto choices = std::vector<std::string> {"Yes", "No"};
|
||||
installDialogId = alertdialog::start(appInstanceId, "Install?", message, choices);
|
||||
#endif
|
||||
} else if (isSupportedImageFile(filename)) {
|
||||
imageviewer::start(processed_filepath);
|
||||
imageviewer::start(file_path);
|
||||
} else if (isSupportedTextFile(filename)) {
|
||||
if (kernel::getPlatform() == kernel::PlatformEsp) {
|
||||
notes::start(processed_filepath);
|
||||
notes::start(file_path);
|
||||
} else {
|
||||
// Remove forward slash, because we need a relative path
|
||||
notes::start(processed_filepath.substr(1));
|
||||
notes::start(file_path.substr(1));
|
||||
}
|
||||
} else {
|
||||
LOG_W(TAG, "Opening files of this type is not supported");
|
||||
|
||||
@@ -1,18 +1,23 @@
|
||||
#include "Tactility/app/fileselection/FileSelection.h"
|
||||
#include "Tactility/app/fileselection/FileSelectionPrivate.h"
|
||||
#include "Tactility/app/fileselection/View.h"
|
||||
#include "Tactility/app/fileselection/State.h"
|
||||
|
||||
#include <app/event.h>
|
||||
#include <app/io.h>
|
||||
#include <app/manager.h>
|
||||
#include <app/manifest.h>
|
||||
#include <app/scheduler.h>
|
||||
#include <app/stream.h>
|
||||
|
||||
#include <lvgl_window_manager/window_manager.h>
|
||||
|
||||
#include <tactility/check.h>
|
||||
|
||||
#include <cstdio>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <unistd.h>
|
||||
|
||||
namespace tt::app::fileselection {
|
||||
|
||||
@@ -27,20 +32,11 @@ struct Context {
|
||||
Mode mode;
|
||||
std::shared_ptr<State> state;
|
||||
std::unique_ptr<View> view;
|
||||
// The eventual appMain() return value - see AlertDialog.cpp's Context::result for why this
|
||||
// is a plain (non-atomic) field safely shared between the LVGL thread (writer, before
|
||||
// emitting APP_EVENT_CLOSE) and this app's own thread (reader, after waking from it).
|
||||
int32_t result = 1; // Cancelled - safety-net default if closed without picking a file
|
||||
std::string resultPath;
|
||||
int32_t resultCode = 1; // 1 means Cancelled
|
||||
};
|
||||
|
||||
|
||||
// The last picked path. Static rather than per-instance: simple, and in practice only one
|
||||
// FileSelection dialog is ever open at a time. Written on the LVGL thread (View's select-button
|
||||
// callback, before emitting APP_EVENT_CLOSE); read by the parent via getLastPath() after
|
||||
// receiving that event - safe without a lock for the same reason Context::result is (see
|
||||
// AlertDialog.cpp).
|
||||
std::string lastPath;
|
||||
|
||||
void createWidgets(lv_obj_t* parent, void* userData) {
|
||||
auto* ctx = static_cast<Context*>(userData);
|
||||
ctx->view->init(parent, ctx->mode);
|
||||
@@ -52,16 +48,11 @@ int32_t appMain(int argc, char* argv[]) {
|
||||
|
||||
Context ctx {};
|
||||
ctx.appInstanceId = appInstanceId;
|
||||
ctx.mode = (argc > 0 && std::string(argv[0]) == "existing_or_new") ? Mode::ExistingOrNew : Mode::Existing;
|
||||
ctx.mode = (argc > 0 && std::string(argv[0]) == "--existing-or-new") ? Mode::ExistingOrNew : Mode::Existing;
|
||||
ctx.state = std::make_shared<State>();
|
||||
ctx.view = std::make_unique<View>(appInstanceId, ctx.state, [&ctx, appInstanceId](const std::string& path) {
|
||||
// Runs on the LVGL task (View::onSelectButtonPressed) - must NOT call app_manager_stop()
|
||||
// here: that bound-waits (thread_join) for this app's own thread to finish, which needs
|
||||
// the LVGL lock (window_manager_remove()) - but this callback runs ON the LVGL task,
|
||||
// which would deadlock against itself. The caller reaps this instance via
|
||||
// app_manager_stop() after it receives the APP_EVENT_RESULT instead.
|
||||
lastPath = path;
|
||||
ctx.result = 0;
|
||||
ctx.resultPath = path;
|
||||
ctx.resultCode = 0;
|
||||
app_event_emit_close(appInstanceId);
|
||||
});
|
||||
|
||||
@@ -91,27 +82,42 @@ int32_t appMain(int argc, char* argv[]) {
|
||||
check(app_event_unsubscribe(&sub) == ERROR_NONE);
|
||||
task_event_group_destruct(&event_group);
|
||||
|
||||
return ctx.result;
|
||||
if (ctx.resultCode == 0) {
|
||||
// The parent captures this via an AppStream bound to our stdout (see startWithMode()) -
|
||||
// see AppStdioWrap.cpp for how printf() itself gets routed there on POSIX.
|
||||
LOG_I(TAG, "Result: %s", ctx.resultPath.c_str());
|
||||
printf("%s", ctx.resultPath.c_str());
|
||||
}
|
||||
|
||||
return ctx.resultCode;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
std::string getLastPath() {
|
||||
return lastPath;
|
||||
}
|
||||
namespace {
|
||||
|
||||
uint32_t startForExistingFile(uint32_t callerAppInstanceId) {
|
||||
const char* argv[] = { "existing" };
|
||||
uint32_t startWithMode(const char* modeArg, uint32_t callerAppInstanceId, AppStream& stream, void* buffer, size_t bufferCapacity, TaskEventGroup* eventGroup) {
|
||||
const char* argv[] = { modeArg };
|
||||
AppStreamBinding binding = {
|
||||
.producer_fd = STDOUT_FILENO,
|
||||
.stream = &stream,
|
||||
.buffer = buffer,
|
||||
.buffer_capacity = bufferCapacity,
|
||||
.event_group = eventGroup,
|
||||
};
|
||||
uint32_t instanceId = 0;
|
||||
app_manager_start_for_result(manifest.id, callerAppInstanceId, 1, argv, &instanceId);
|
||||
app_manager_start_for_result_with_streams(manifest.id, callerAppInstanceId, 1, argv, &binding, 1, &instanceId);
|
||||
return instanceId;
|
||||
}
|
||||
|
||||
uint32_t startForExistingOrNewFile(uint32_t callerAppInstanceId) {
|
||||
const char* argv[] = { "existing_or_new" };
|
||||
uint32_t instanceId = 0;
|
||||
app_manager_start_for_result(manifest.id, callerAppInstanceId, 1, argv, &instanceId);
|
||||
return instanceId;
|
||||
} // namespace
|
||||
|
||||
uint32_t startForExistingFile(uint32_t callerAppInstanceId, AppStream& stream, void* buffer, size_t bufferCapacity, TaskEventGroup* eventGroup) {
|
||||
return startWithMode("--existing", callerAppInstanceId, stream, buffer, bufferCapacity, eventGroup);
|
||||
}
|
||||
|
||||
uint32_t startForExistingOrNewFile(uint32_t callerAppInstanceId, AppStream& stream, void* buffer, size_t bufferCapacity, TaskEventGroup* eventGroup) {
|
||||
return startWithMode("--existing-or-new", callerAppInstanceId, stream, buffer, bufferCapacity, eventGroup);
|
||||
}
|
||||
|
||||
extern const ::AppManifest manifest = {
|
||||
|
||||
@@ -33,7 +33,7 @@ std::string State::getSelectedChildPath() const {
|
||||
}
|
||||
|
||||
bool State::setEntriesForPath(const std::string& path) {
|
||||
LOG_I(TAG, "Changing path: %s -> %s", current_path.c_str(), path.c_str());
|
||||
LOG_D(TAG, "Changing path: %s -> %s", current_path.c_str(), path.c_str());
|
||||
|
||||
auto lock = mutex.asScopedLock();
|
||||
if (!lock.lock(100)) {
|
||||
@@ -47,7 +47,6 @@ bool State::setEntriesForPath(const std::string& path) {
|
||||
*/
|
||||
bool show_custom_root = (kernel::getPlatform() == kernel::PlatformEsp) && (path == "/");
|
||||
if (show_custom_root) {
|
||||
LOG_I(TAG, "Setting custom root");
|
||||
dir_entries = file::getFileSystemDirents();
|
||||
current_path = path;
|
||||
selected_child_entry = "";
|
||||
@@ -56,7 +55,6 @@ bool State::setEntriesForPath(const std::string& path) {
|
||||
dir_entries.clear();
|
||||
int count = file::scandir(path, dir_entries, &file::direntFilterDotEntries, file::direntSortAlphaAndType);
|
||||
if (count >= 0) {
|
||||
LOG_I(TAG, "%s has %d entries", path.c_str(), count);
|
||||
current_path = path;
|
||||
selected_child_entry = "";
|
||||
return true;
|
||||
@@ -69,7 +67,7 @@ bool State::setEntriesForPath(const std::string& path) {
|
||||
|
||||
bool State::setEntriesForChildPath(const std::string& childPath) {
|
||||
auto path = file::getChildPath(current_path, childPath);
|
||||
LOG_I(TAG, "Navigating from %s to %s", current_path.c_str(), path.c_str());
|
||||
LOG_D(TAG, "Navigating from %s to %s", current_path.c_str(), path.c_str());
|
||||
return setEntriesForPath(path);
|
||||
}
|
||||
|
||||
|
||||
@@ -60,12 +60,15 @@ void View::onTapFile(const std::string& path, const std::string& filename) {
|
||||
LOG_E(TAG, "Can only work with files in working directory %s", cwd);
|
||||
return;
|
||||
}
|
||||
processed_filepath = file_path.substr(strlen(cwd));
|
||||
// MountPoints.h's MOUNT_POINT_DATA/MOUNT_POINT_SYSTEM have no leading slash on POSIX
|
||||
// (fopen() resolves relative to cwd there), unlike ESP32's real VFS mount points - so
|
||||
// strip the separator too, not just cwd itself.
|
||||
processed_filepath = file_path.substr(strlen(cwd) + 1);
|
||||
} else {
|
||||
processed_filepath = file_path;
|
||||
}
|
||||
|
||||
LOG_I(TAG, "Clicked %s", processed_filepath.c_str());
|
||||
LOG_D(TAG, "Clicked %s", processed_filepath.c_str());
|
||||
|
||||
lv_textarea_set_text(path_textarea, processed_filepath.c_str());
|
||||
}
|
||||
@@ -73,7 +76,7 @@ void View::onTapFile(const std::string& path, const std::string& filename) {
|
||||
void View::onDirEntryPressed(uint32_t index) {
|
||||
dirent dir_entry;
|
||||
if (state->getDirent(index, dir_entry)) {
|
||||
LOG_I(TAG, "Pressed %s %d", dir_entry.d_name, (int)dir_entry.d_type);
|
||||
LOG_D(TAG, "Pressed %s %d", dir_entry.d_name, (int)dir_entry.d_type);
|
||||
state->setSelectedChildEntry(dir_entry.d_name);
|
||||
using namespace tt::file;
|
||||
switch (dir_entry.d_type) {
|
||||
@@ -146,7 +149,7 @@ void View::createDirEntryWidget(lv_obj_t* list, dirent& dir_entry) {
|
||||
|
||||
void View::onNavigateUpPressed() {
|
||||
if (state->getCurrentPath() != "/") {
|
||||
LOG_I(TAG, "Navigating upwards");
|
||||
LOG_D(TAG, "Navigating upwards");
|
||||
std::string new_absolute_path;
|
||||
if (string::getPathParent(state->getCurrentPath(), new_absolute_path)) {
|
||||
state->setEntriesForPath(new_absolute_path);
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
#include <Tactility/app/notes/Notes.h>
|
||||
|
||||
#include "Tactility/app/alertdialog/AlertDialog.h"
|
||||
|
||||
#include <Tactility/app/fileselection/FileSelection.h>
|
||||
#include <Tactility/file/File.h>
|
||||
|
||||
@@ -6,6 +9,7 @@
|
||||
#include <app/manager.h>
|
||||
#include <app/manifest.h>
|
||||
#include <app/scheduler.h>
|
||||
#include <app/stream.h>
|
||||
|
||||
#include <lvgl_window_manager/window_manager.h>
|
||||
|
||||
@@ -25,6 +29,7 @@ namespace {
|
||||
|
||||
struct Context {
|
||||
uint32_t appInstanceId;
|
||||
TaskEventGroup* eventGroup = nullptr;
|
||||
|
||||
lv_obj_t* uiCurrentFileName = nullptr;
|
||||
lv_obj_t* uiDropDownMenu = nullptr;
|
||||
@@ -35,6 +40,10 @@ struct Context {
|
||||
|
||||
uint32_t loadFileLaunchId = 0;
|
||||
uint32_t saveFileLaunchId = 0;
|
||||
AppStream loadResultStream {};
|
||||
uint8_t loadResultBuffer[256] {};
|
||||
AppStream saveResultStream {};
|
||||
uint8_t saveResultBuffer[256] {};
|
||||
};
|
||||
|
||||
|
||||
@@ -90,11 +99,11 @@ void appNotesEventCb(lv_event_t* e) {
|
||||
lvgl_lock();
|
||||
ctx->saveBuffer = lv_textarea_get_text(ctx->uiNoteText);
|
||||
lvgl_unlock();
|
||||
ctx->saveFileLaunchId = fileselection::startForExistingOrNewFile(ctx->appInstanceId);
|
||||
ctx->saveFileLaunchId = fileselection::startForExistingOrNewFile(ctx->appInstanceId, ctx->saveResultStream, ctx->saveResultBuffer, sizeof(ctx->saveResultBuffer), ctx->eventGroup);
|
||||
LOG_I(TAG, "launched with id %u", ctx->saveFileLaunchId);
|
||||
break;
|
||||
case 3: // Load
|
||||
ctx->loadFileLaunchId = fileselection::startForExistingFile(ctx->appInstanceId);
|
||||
ctx->loadFileLaunchId = fileselection::startForExistingFile(ctx->appInstanceId, ctx->loadResultStream, ctx->loadResultBuffer, sizeof(ctx->loadResultBuffer), ctx->eventGroup);
|
||||
LOG_I(TAG, "launched with id %u", ctx->loadFileLaunchId);
|
||||
break;
|
||||
}
|
||||
@@ -102,7 +111,7 @@ void appNotesEventCb(lv_event_t* e) {
|
||||
auto* cont = lv_event_get_current_target_obj(e);
|
||||
if (obj == cont) return;
|
||||
if (lv_obj_get_child(cont, 1)) {
|
||||
ctx->saveFileLaunchId = fileselection::startForExistingOrNewFile(ctx->appInstanceId);
|
||||
ctx->saveFileLaunchId = fileselection::startForExistingOrNewFile(ctx->appInstanceId, ctx->saveResultStream, ctx->saveResultBuffer, sizeof(ctx->saveResultBuffer), ctx->eventGroup);
|
||||
LOG_I(TAG, "launched with id %u", ctx->saveFileLaunchId);
|
||||
} else { //Reset
|
||||
resetFileContent(ctx);
|
||||
@@ -189,6 +198,7 @@ int32_t appMain(int argc, char* argv[]) {
|
||||
|
||||
TaskEventGroup event_group {};
|
||||
task_event_group_construct(&event_group);
|
||||
ctx.eventGroup = &event_group;
|
||||
|
||||
AppEventSubscription sub {};
|
||||
check(app_event_subscribe(&sub, &event_group) == ERROR_NONE);
|
||||
@@ -206,23 +216,35 @@ int32_t appMain(int argc, char* argv[]) {
|
||||
shouldClose = true;
|
||||
break;
|
||||
case APP_EVENT_RESULT:
|
||||
LOG_I(TAG, "Result for launch id %u", event.result.launch_id);
|
||||
LOG_I(TAG, "Result for launch id %u = %u", event.result.launch_id, event.result.result);
|
||||
if (event.result.launch_id == ctx.loadFileLaunchId) {
|
||||
ctx.loadFileLaunchId = 0;
|
||||
if (event.result.result == 0 /* Ok */) {
|
||||
auto path = fileselection::getLastPath();
|
||||
char destination[sizeof(ctx.loadResultBuffer)];
|
||||
size_t length = app_stream_read(&ctx.loadResultStream, destination, sizeof(destination));
|
||||
app_stream_unsubscribe(&ctx.loadResultStream);
|
||||
auto path = std::string(destination, length);
|
||||
LOG_I(TAG, "Path: '%s'", path.c_str());
|
||||
if (!path.empty()) {
|
||||
openFile(&ctx, path);
|
||||
}
|
||||
} else {
|
||||
app_stream_unsubscribe(&ctx.loadResultStream);
|
||||
}
|
||||
} else if (event.result.launch_id == ctx.saveFileLaunchId) {
|
||||
ctx.saveFileLaunchId = 0;
|
||||
if (event.result.result == 0 /* Ok */) {
|
||||
auto path = fileselection::getLastPath();
|
||||
char destination[sizeof(ctx.saveResultBuffer)];
|
||||
size_t length = app_stream_read(&ctx.saveResultStream, destination, sizeof(destination));
|
||||
app_stream_unsubscribe(&ctx.saveResultStream);
|
||||
auto path = std::string(destination, length);
|
||||
// Must re-open file, because the UI was cleared after opening the dialog.
|
||||
LOG_I(TAG, "Path: '%s'", path.c_str());
|
||||
if (!path.empty() && saveFile(&ctx, path)) {
|
||||
openFile(&ctx, path);
|
||||
}
|
||||
} else {
|
||||
app_stream_unsubscribe(&ctx.saveResultStream);
|
||||
}
|
||||
}
|
||||
app_manager_stop(event.result.launch_id);
|
||||
|
||||
@@ -66,7 +66,6 @@ int scandir(
|
||||
ScandirFilter filterMethod,
|
||||
ScandirSort sortMethod
|
||||
) {
|
||||
LOG_I(TAG, "scandir start");
|
||||
DIR* dir = opendir(path.c_str());
|
||||
if (dir == nullptr) {
|
||||
LOG_E(TAG, "Failed to open dir %s", path.c_str());
|
||||
@@ -86,7 +85,6 @@ int scandir(
|
||||
std::ranges::sort(outList, sortMethod);
|
||||
}
|
||||
|
||||
LOG_I(TAG, "scandir finish");
|
||||
return outList.size();
|
||||
}
|
||||
|
||||
|
||||
@@ -19,11 +19,25 @@ target_include_directories(TactilityTests PRIVATE ${CMAKE_CURRENT_LIST_DIR}/../P
|
||||
|
||||
add_test(NAME TactilityTests COMMAND TactilityTests)
|
||||
|
||||
# Matches Tactility/CMakeLists.txt's own set of --wrap flags: this binary also compiles
|
||||
# AppStdioWrap.cpp and links app-module (whose io.cpp calls __real_read/write/close() on
|
||||
# non-Apple POSIX - see TT_APP_IO_WRAPS_STDIO in Modules/app-module/CMakeLists.txt), so it
|
||||
# needs the same wraps applied or those go unresolved.
|
||||
if (NOT APPLE)
|
||||
target_link_options(TactilityTests PRIVATE
|
||||
"-Wl,--wrap=pthread_attr_setstack" "-Wl,--wrap=read" "-Wl,--wrap=write" "-Wl,--wrap=close"
|
||||
"-Wl,--wrap=printf" "-Wl,--wrap=fprintf" "-Wl,--wrap=vprintf" "-Wl,--wrap=vfprintf"
|
||||
"-Wl,--wrap=puts" "-Wl,--wrap=fputs" "-Wl,--wrap=putchar" "-Wl,--wrap=fputc"
|
||||
"-Wl,--wrap=getchar" "-Wl,--wrap=fgetc" "-Wl,--wrap=fgets"
|
||||
)
|
||||
endif ()
|
||||
|
||||
target_link_libraries(TactilityTests PRIVATE
|
||||
TactilityKernel
|
||||
TactilityKernelCpp
|
||||
TactilityFreeRtos
|
||||
platform-posix
|
||||
app-posix-module
|
||||
lvgl-module
|
||||
lvgl-window-manager-module
|
||||
app-module
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#pragma once
|
||||
|
||||
#include <stddef.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
// Fixed capacity of one queued log line (256 bytes * 24-deep queue = 6KB, negligible against the
|
||||
// hundreds of KB of free heap typically available - generous enough to hold a full log line with
|
||||
// color/timestamp/tag prefix plus a realistic message, e.g. a full SD-card path in an error log).
|
||||
#define LOG_QUEUE_MESSAGE_MAX_LENGTH 256U
|
||||
|
||||
/**
|
||||
* Starts the shared log queue and its dedicated drain (writer) task. Idempotent - safe to call
|
||||
* more than once (only the first call has any effect). Must be called as early as possible in
|
||||
* boot - see kernel_init.cpp, which calls this as its very first statement.
|
||||
*/
|
||||
void log_queue_init(void);
|
||||
|
||||
/**
|
||||
* Enqueues an already-fully-formatted, ready-to-write log line (NOT a printf-style format
|
||||
* string) so the dedicated drain task can write it via a path that never goes through
|
||||
* app-module's fd-table redirection (see Modules/app-module/source/io.cpp's app_io_write()) -
|
||||
* this is the ONLY function log-producing code should call to reach the console; it must never
|
||||
* itself call write()/printf()/etc, since those would be intercepted whenever called from an app
|
||||
* instance's own task, which is exactly the bug this exists to structurally prevent.
|
||||
*
|
||||
* Non-blocking: if the queue is full, the message is dropped and a counter is incremented; the
|
||||
* drain task prepends a "N messages dropped" notice before its next write. If called before
|
||||
* log_queue_init() has run, performs a direct synchronous write instead of enqueueing - safe,
|
||||
* since no app instance (and so no fd-table redirection) can exist yet that early in boot.
|
||||
* Truncates messages longer than LOG_QUEUE_MESSAGE_MAX_LENGTH - 1.
|
||||
*/
|
||||
void log_queue_write(const char* data, size_t length);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include <tactility/device.h>
|
||||
#include <tactility/log.h>
|
||||
#include <tactility/log_queue.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
@@ -46,6 +47,8 @@ Module kernel_module = {
|
||||
};
|
||||
|
||||
error_t kernel_init(Module* const dts_modules[], const DtsDevice dts_devices[]) {
|
||||
log_queue_init();
|
||||
|
||||
LOG_I(TAG, "init");
|
||||
|
||||
if (module_construct_add_start(&kernel_module) != ERROR_NONE) {
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#ifndef ESP_PLATFORM
|
||||
|
||||
#include <tactility/log.h>
|
||||
#include <tactility/log_queue.h>
|
||||
|
||||
#include <mutex>
|
||||
#include <inttypes.h>
|
||||
@@ -11,7 +12,11 @@
|
||||
#include <stdarg.h>
|
||||
#include <sys/time.h>
|
||||
|
||||
static const char* get_log_color(LogLevel level) {
|
||||
namespace {
|
||||
|
||||
constexpr auto MINIMUM_LOG_LEVEL = LOG_LEVEL_DEBUG;
|
||||
|
||||
const char* get_log_color(LogLevel level) {
|
||||
using enum LogLevel;
|
||||
switch (level) {
|
||||
case LOG_LEVEL_ERROR:
|
||||
@@ -29,7 +34,7 @@ static const char* get_log_color(LogLevel level) {
|
||||
}
|
||||
}
|
||||
|
||||
static inline char get_log_prefix(LogLevel level) {
|
||||
inline char get_log_prefix(LogLevel level) {
|
||||
using enum LogLevel;
|
||||
switch (level) {
|
||||
case LOG_LEVEL_ERROR:
|
||||
@@ -47,7 +52,7 @@ static inline char get_log_prefix(LogLevel level) {
|
||||
}
|
||||
}
|
||||
|
||||
static uint64_t get_log_timestamp() {
|
||||
uint64_t get_log_timestamp() {
|
||||
static uint64_t base = 0U;
|
||||
static std::once_flag init_flag;
|
||||
std::call_once(init_flag, []() {
|
||||
@@ -61,15 +66,42 @@ static uint64_t get_log_timestamp() {
|
||||
return now - base;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extern "C" {
|
||||
|
||||
void log_generic(enum LogLevel level, const char* tag, const char* format, ...) {
|
||||
if (MINIMUM_LOG_LEVEL >= level) {
|
||||
char buffer[LOG_QUEUE_MESSAGE_MAX_LENGTH];
|
||||
size_t offset = 0;
|
||||
|
||||
int prefix_len = snprintf(buffer, sizeof(buffer), "%s %c (%" PRIu64 ") %s ",
|
||||
get_log_color(level), get_log_prefix(level), get_log_timestamp(), tag);
|
||||
if (prefix_len > 0) {
|
||||
offset = static_cast<size_t>(prefix_len) < sizeof(buffer) ? static_cast<size_t>(prefix_len) : sizeof(buffer) - 1;
|
||||
}
|
||||
|
||||
if (offset < sizeof(buffer)) {
|
||||
va_list args;
|
||||
va_start(args, format);
|
||||
printf("%s %c (%" PRIu64 ") %s ", get_log_color(level), get_log_prefix(level), get_log_timestamp(), tag);
|
||||
vprintf(format, args);
|
||||
printf("\033[0m\n");
|
||||
int written = vsnprintf(buffer + offset, sizeof(buffer) - offset, format, args);
|
||||
va_end(args);
|
||||
if (written > 0) {
|
||||
size_t remaining = sizeof(buffer) - offset;
|
||||
offset += static_cast<size_t>(written) < remaining ? static_cast<size_t>(written) : remaining - 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (offset < sizeof(buffer)) {
|
||||
int tail_len = snprintf(buffer + offset, sizeof(buffer) - offset, "\033[0m\n");
|
||||
if (tail_len > 0) {
|
||||
size_t remaining = sizeof(buffer) - offset;
|
||||
offset += static_cast<size_t>(tail_len) < remaining ? static_cast<size_t>(tail_len) : remaining - 1;
|
||||
}
|
||||
}
|
||||
|
||||
log_queue_write(buffer, offset);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,184 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#include <tactility/log_queue.h>
|
||||
|
||||
#include <tactility/freertos/queue.h>
|
||||
#include <tactility/freertos/task.h>
|
||||
#include <tactility/memory.h>
|
||||
|
||||
#include <atomic>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <unistd.h>
|
||||
|
||||
#if defined(ESP_PLATFORM)
|
||||
#include <esp_log.h>
|
||||
#endif
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr size_t LOG_QUEUE_DEPTH = 8;
|
||||
constexpr size_t DRAIN_TASK_STACK_DEPTH = 4096 / sizeof(StackType_t);
|
||||
|
||||
struct LogQueueMessage {
|
||||
uint16_t length;
|
||||
char text[LOG_QUEUE_MESSAGE_MAX_LENGTH];
|
||||
};
|
||||
|
||||
std::atomic<QueueHandle_t> g_queue { nullptr };
|
||||
|
||||
// The one and only place a real console write happens for log output - deliberately never
|
||||
// through app_io_write()'s fd-table redirection (Modules/app-module/source/io.cpp). That
|
||||
// redirection only ever triggers for a task app_task_main() (Modules/app-module/source/
|
||||
// scheduler.cpp) set up as a running app instance; this drain task is never that, so
|
||||
// app_scheduler_current_app_id() always reads 0 on it and app_io_write()'s lookup is skipped
|
||||
// unconditionally - a plain write() already reaches the real syscall, on every platform, with no
|
||||
// wrap-bypassing needed here.
|
||||
void write_real(const char* data, size_t length) {
|
||||
if (length == 0) {
|
||||
return;
|
||||
}
|
||||
#if defined(ESP_PLATFORM)
|
||||
constexpr int fd = 1; // ESP-IDF's default console fd
|
||||
#else
|
||||
constexpr int fd = 2; // matches log_generic()'s stderr choice
|
||||
#endif
|
||||
::write(fd, data, length);
|
||||
}
|
||||
|
||||
void drain_task_main(void* context) {
|
||||
// The queue handle is already valid by the time this task starts (it's created before the
|
||||
// task is), but reading it back from g_queue here would race: g_queue is only published
|
||||
// (log_queue_init()'s g_queue.store()) AFTER task creation returns, so this task can start
|
||||
// running before that store happens. Take it directly as the task's own argument instead.
|
||||
auto queue = static_cast<QueueHandle_t>(context);
|
||||
LogQueueMessage message;
|
||||
while (true) {
|
||||
if (xQueueReceive(queue, &message, portMAX_DELAY) == pdTRUE) {
|
||||
write_real(message.text, message.length);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#if defined(ESP_PLATFORM)
|
||||
// ESP-IDF's own log macros pre-format the entire line (color, level letter, timestamp, tag,
|
||||
// message, color reset, newline - see esp_log_write()/LOG_FORMAT() in esp_log.h) before handing
|
||||
// it to the installed vprintf_like_t as fmt+args, so this hook only needs one vsnprintf - no
|
||||
// prefix building of its own (unlike log_generic() in log.cpp, which builds its own prefix).
|
||||
// Replaces (does not chain through) the previously-installed vprintf: the whole point is
|
||||
// removing the direct-to-stdout path, not adding a second consumer of it.
|
||||
int log_queue_vprintf_hook(const char* format, va_list args) {
|
||||
char buffer[LOG_QUEUE_MESSAGE_MAX_LENGTH];
|
||||
int written = vsnprintf(buffer, sizeof(buffer), format, args);
|
||||
if (written > 0) {
|
||||
size_t length = static_cast<size_t>(written) < sizeof(buffer) ? static_cast<size_t>(written) : sizeof(buffer) - 1;
|
||||
log_queue_write(buffer, length);
|
||||
}
|
||||
return written;
|
||||
}
|
||||
#endif
|
||||
|
||||
} // namespace
|
||||
|
||||
extern "C" {
|
||||
|
||||
void log_queue_init(void) {
|
||||
if (g_queue.load(std::memory_order_relaxed) != nullptr) {
|
||||
return; // already initialized
|
||||
}
|
||||
|
||||
#if defined(ESP_PLATFORM)
|
||||
// Message storage: prefer PSRAM/external memory, fall back to internal automatically if
|
||||
// unavailable (MemoryPolicy's documented semantics) - this is the bulk of the queue's memory
|
||||
// (24 * 256 bytes), so it's the part worth offloading to PSRAM when there is any.
|
||||
MemoryPolicy storage_policy = { .required = 0, .desired = MEMORY_CAPABILITY_EXTERNAL, .alignment = 0 };
|
||||
auto* queue_storage = static_cast<uint8_t*>(memory_alloc_with_policy(LOG_QUEUE_DEPTH * sizeof(LogQueueMessage), &storage_policy));
|
||||
if (queue_storage == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
// The queue's own control block and the drain task's TCB are both small, fixed-size kernel
|
||||
// objects (not the bulk data) - keep them in internal RAM like scheduler.cpp's
|
||||
// APP_TASK_TCB_POLICY already does for app instance tasks.
|
||||
MemoryPolicy internal_policy = { .required = MEMORY_CAPABILITY_INTERNAL, .desired = 0, .alignment = 0 };
|
||||
|
||||
auto* queue_struct = static_cast<StaticQueue_t*>(memory_alloc_with_policy(sizeof(StaticQueue_t), &internal_policy));
|
||||
if (queue_struct == nullptr) {
|
||||
memory_free(queue_storage);
|
||||
return;
|
||||
}
|
||||
|
||||
QueueHandle_t queue = xQueueCreateStatic(LOG_QUEUE_DEPTH, sizeof(LogQueueMessage), queue_storage, queue_struct);
|
||||
if (queue == nullptr) {
|
||||
memory_free(queue_struct);
|
||||
memory_free(queue_storage);
|
||||
return;
|
||||
}
|
||||
|
||||
// Stack: same PSRAM-preferred/internal-fallback policy as the message storage above.
|
||||
MemoryPolicy stack_policy = { .required = 0, .desired = MEMORY_CAPABILITY_EXTERNAL, .alignment = 0 };
|
||||
auto* stack_buffer = static_cast<StackType_t*>(memory_alloc_with_policy(DRAIN_TASK_STACK_DEPTH * sizeof(StackType_t), &stack_policy));
|
||||
if (stack_buffer == nullptr) {
|
||||
vQueueDelete(queue);
|
||||
memory_free(queue_struct);
|
||||
memory_free(queue_storage);
|
||||
return;
|
||||
}
|
||||
|
||||
auto* task_tcb = static_cast<StaticTask_t*>(memory_alloc_with_policy(sizeof(StaticTask_t), &internal_policy));
|
||||
if (task_tcb == nullptr) {
|
||||
memory_free(stack_buffer);
|
||||
vQueueDelete(queue);
|
||||
memory_free(queue_struct);
|
||||
memory_free(queue_storage);
|
||||
return;
|
||||
}
|
||||
|
||||
TaskHandle_t task_handle = xTaskCreateStatic(drain_task_main, "log_drain", DRAIN_TASK_STACK_DEPTH, queue, tskIDLE_PRIORITY + 1, stack_buffer, task_tcb);
|
||||
if (task_handle == nullptr) {
|
||||
memory_free(task_tcb);
|
||||
memory_free(stack_buffer);
|
||||
vQueueDelete(queue);
|
||||
memory_free(queue_struct);
|
||||
memory_free(queue_storage);
|
||||
return;
|
||||
}
|
||||
#else
|
||||
QueueHandle_t queue = xQueueCreate(LOG_QUEUE_DEPTH, sizeof(LogQueueMessage));
|
||||
if (queue == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
TaskHandle_t task_handle = nullptr;
|
||||
if (xTaskCreate(drain_task_main, "log_drain", DRAIN_TASK_STACK_DEPTH, queue, tskIDLE_PRIORITY + 1, &task_handle) != pdPASS) {
|
||||
vQueueDelete(queue);
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
|
||||
g_queue.store(queue, std::memory_order_release); // published last
|
||||
|
||||
#if defined(ESP_PLATFORM)
|
||||
esp_log_set_vprintf(log_queue_vprintf_hook);
|
||||
#endif
|
||||
}
|
||||
|
||||
void log_queue_write(const char* data, size_t length) {
|
||||
if (data == nullptr || length == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
QueueHandle_t queue = g_queue.load(std::memory_order_acquire);
|
||||
if (queue == nullptr) {
|
||||
write_real(data, length); // pre-init fallback
|
||||
return;
|
||||
}
|
||||
|
||||
LogQueueMessage message;
|
||||
size_t copy_length = length < sizeof(message.text) ? length : sizeof(message.text) - 1;
|
||||
memcpy(message.text, data, copy_length);
|
||||
message.length = static_cast<uint16_t>(copy_length);
|
||||
|
||||
xQueueSend(queue, &message, portMAX_DELAY);
|
||||
}
|
||||
|
||||
} // extern "C"
|
||||
@@ -42,7 +42,6 @@ static error_t paths_get_data_root_path(char* out_path, size_t out_path_size) {
|
||||
extern "C" {
|
||||
|
||||
error_t paths_get_data_path(char* out_path, size_t out_path_size) {
|
||||
#ifdef ESP_PLATFORM
|
||||
char root[64];
|
||||
error_t error = paths_get_data_root_path(root, sizeof(root));
|
||||
if (error != ERROR_NONE) {
|
||||
@@ -53,14 +52,6 @@ error_t paths_get_data_path(char* out_path, size_t out_path_size) {
|
||||
return ERROR_BUFFER_OVERFLOW;
|
||||
}
|
||||
return ERROR_NONE;
|
||||
#else
|
||||
const char* fixed_path = "data";
|
||||
if (std::strlen(fixed_path) + 1 > out_path_size) {
|
||||
return ERROR_BUFFER_OVERFLOW;
|
||||
}
|
||||
std::strcpy(out_path, fixed_path);
|
||||
return ERROR_NONE;
|
||||
#endif
|
||||
}
|
||||
|
||||
} // extern "C"
|
||||
|
||||
@@ -5,22 +5,22 @@
|
||||
#include <tactility/paths.h>
|
||||
|
||||
// The simulator target is never built with ESP_PLATFORM, so paths_get_data_path()
|
||||
// always takes the fixed "data" path branch here, guarded by a buffer-size check.
|
||||
// always takes the fixed "data" root, with "/tactility" appended, guarded by a buffer-size check.
|
||||
|
||||
TEST_CASE("paths_get_data_path succeeds when the buffer exactly fits") {
|
||||
char buffer[16] = { 0 };
|
||||
char buffer[32] = { 0 };
|
||||
CHECK_EQ(paths_get_data_path(buffer, sizeof(buffer)), ERROR_NONE);
|
||||
CHECK_EQ(std::strcmp(buffer, "data"), 0);
|
||||
CHECK_EQ(std::strcmp(buffer, "data/tactility"), 0);
|
||||
}
|
||||
|
||||
TEST_CASE("paths_get_data_path succeeds with a buffer sized to exactly fit the string and terminator") {
|
||||
char buffer[5] = { 0 }; // strlen("data") + 1
|
||||
char buffer[15] = { 0 }; // strlen("data/tactility") + 1
|
||||
CHECK_EQ(paths_get_data_path(buffer, sizeof(buffer)), ERROR_NONE);
|
||||
CHECK_EQ(std::strcmp(buffer, "data"), 0);
|
||||
CHECK_EQ(std::strcmp(buffer, "data/tactility"), 0);
|
||||
}
|
||||
|
||||
TEST_CASE("paths_get_data_path reports a buffer overflow when the buffer is one byte too small") {
|
||||
char buffer[4] = { 0 }; // strlen("data"), no room for the terminator
|
||||
char buffer[14] = { 0 }; // strlen("data/tactility"), no room for the terminator
|
||||
CHECK_EQ(paths_get_data_path(buffer, sizeof(buffer)), ERROR_BUFFER_OVERFLOW);
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ add_subdirectory(${CMAKE_SOURCE_DIR}/TactilityKernel/tests ${CMAKE_CURRENT_BINAR
|
||||
add_subdirectory(${CMAKE_SOURCE_DIR}/Tactility/Tests ${CMAKE_CURRENT_BINARY_DIR}/Tactility)
|
||||
add_subdirectory(${CMAKE_SOURCE_DIR}/Modules/crypt-module/tests ${CMAKE_CURRENT_BINARY_DIR}/crypt-module)
|
||||
add_subdirectory(${CMAKE_SOURCE_DIR}/Modules/app-module/tests ${CMAKE_CURRENT_BINARY_DIR}/app-module)
|
||||
add_subdirectory(${CMAKE_SOURCE_DIR}/Modules/app-posix-module/tests ${CMAKE_CURRENT_BINARY_DIR}/app-posix-module)
|
||||
add_subdirectory(${CMAKE_SOURCE_DIR}/Modules/http-module/tests ${CMAKE_CURRENT_BINARY_DIR}/http-module)
|
||||
|
||||
add_custom_target(build-tests)
|
||||
@@ -18,4 +19,5 @@ add_dependencies(build-tests TactilityTests)
|
||||
add_dependencies(build-tests TactilityKernelTests)
|
||||
add_dependencies(build-tests CryptModuleTests)
|
||||
add_dependencies(build-tests AppModuleTests)
|
||||
add_dependencies(build-tests AppPosixModuleTests)
|
||||
add_dependencies(build-tests HttpModuleTests)
|
||||
|
||||
@@ -1,16 +1,13 @@
|
||||
# tactility-cmakelists-version: 1
|
||||
cmake_minimum_required(VERSION 3.20)
|
||||
|
||||
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
|
||||
|
||||
if (DEFINED ENV{TACTILITY_SDK_PATH})
|
||||
set(TACTILITY_SDK_PATH $ENV{TACTILITY_SDK_PATH})
|
||||
else()
|
||||
set(TACTILITY_SDK_PATH ../../release/TactilitySDK)
|
||||
message(WARNING "TACTILITY_SDK_PATH environment variable is not set, defaulting to ${TACTILITY_SDK_PATH}")
|
||||
if (NOT DEFINED ENV{TACTILITY_SDK_PATH})
|
||||
message(FATAL_ERROR "TACTILITY_SDK_PATH environment variable is not set")
|
||||
endif()
|
||||
|
||||
get_filename_component(TACTILITY_SDK_PATH "$ENV{TACTILITY_SDK_PATH}" ABSOLUTE)
|
||||
include("${TACTILITY_SDK_PATH}/TactilitySDK.cmake")
|
||||
set(EXTRA_COMPONENT_DIRS ${TACTILITY_SDK_PATH} ${TACTILITY_SDK_PATH}/Modules)
|
||||
|
||||
project(SdkTest)
|
||||
tactility_project(SdkTest)
|
||||
tactility_project_pre(tactility.sdktest)
|
||||
project(tactility.sdktest)
|
||||
tactility_project_post(tactility.sdktest)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
file(GLOB_RECURSE SOURCE_FILES Source/*.c)
|
||||
if (NOT DEFINED TACTILITY_SDK_PATH)
|
||||
get_filename_component(TACTILITY_SDK_PATH "$ENV{TACTILITY_SDK_PATH}" ABSOLUTE BASE_DIR "${CMAKE_CURRENT_LIST_DIR}/..")
|
||||
endif ()
|
||||
include("${TACTILITY_SDK_PATH}/TactilitySDK.cmake")
|
||||
|
||||
idf_component_register(
|
||||
SRCS ${SOURCE_FILES}
|
||||
REQUIRES TactilitySDK
|
||||
app-module crypt-module gps-module lvgl-module lvgl-window-manager-module service-module
|
||||
)
|
||||
file(GLOB_RECURSE SOURCE_FILES Source/*.c)
|
||||
tactility_component_register(SRCS ${SOURCE_FILES} INCLUDE_DIRS include)
|
||||
@@ -9,6 +9,8 @@
|
||||
#include <lvgl.h>
|
||||
#include <lvgl/widgets/toolbar.h>
|
||||
|
||||
#include <greeting.h>
|
||||
|
||||
#include <stdbool.h>
|
||||
|
||||
static void create_widgets(lv_obj_t* parent, void* userData) {
|
||||
@@ -16,7 +18,7 @@ static void create_widgets(lv_obj_t* parent, void* userData) {
|
||||
lv_obj_align(toolbar, LV_ALIGN_TOP_MID, 0, 0);
|
||||
|
||||
lv_obj_t* label = lv_label_create(parent);
|
||||
lv_label_set_text(label, "Hello, world!");
|
||||
lv_label_set_text(label, SDK_TEST_GREETING);
|
||||
lv_obj_align(label, LV_ALIGN_CENTER, 0, 0);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
#pragma once
|
||||
|
||||
// Only exists to prove tactility_component_register()'s INCLUDE_DIRS parameter resolves this
|
||||
// directory on both platforms - see main/CMakeLists.txt.
|
||||
#define SDK_TEST_GREETING "Hello, world!"
|
||||
@@ -1,7 +1,7 @@
|
||||
manifest.version=0.2
|
||||
target.sdk=0.0.0
|
||||
target.platforms=esp32,esp32s3,esp32c6,esp32p4
|
||||
app.id=one.tactility.sdktest
|
||||
target.platforms=esp32,esp32s3,esp32c6,esp32p4,posix-x86_64
|
||||
app.id=tactility.sdktest
|
||||
app.version.name=0.1.0
|
||||
app.version.code=1
|
||||
app.name=SDK Test
|
||||
|
||||
@@ -12,7 +12,7 @@ import tarfile
|
||||
from urllib.parse import urlparse
|
||||
|
||||
ttbuild_path = ".tactility"
|
||||
ttbuild_version = "4.1.0"
|
||||
ttbuild_version = "5.0.0"
|
||||
ttbuild_cdn = "https://cdn.tactilityproject.org"
|
||||
ttbuild_sdk_json_validity = 3600 # seconds
|
||||
ttport = 6666
|
||||
@@ -20,6 +20,10 @@ verbose = False
|
||||
use_local_sdk = False
|
||||
local_base_path = None
|
||||
http_timeout_seconds = 10
|
||||
# App install uploads the whole package over HTTP and the device only responds once it's
|
||||
# fully received, extracted and registered - large packages (e.g. bundled fonts/assets) can
|
||||
# easily take well over http_timeout_seconds on a slow SD card, so give it a lot more room.
|
||||
install_timeout_seconds = 120
|
||||
|
||||
shell_color_red = "\033[91m"
|
||||
shell_color_orange = "\033[93m"
|
||||
@@ -183,6 +187,7 @@ def update_tool_json():
|
||||
|
||||
def should_fetch_sdkconfig_files(platform_targets):
|
||||
for platform in platform_targets:
|
||||
if not platform.startswith("posix"):
|
||||
sdkconfig_filename = f"sdkconfig.app.{platform}"
|
||||
if not os.path.exists(os.path.join(ttbuild_path, sdkconfig_filename)):
|
||||
return True
|
||||
@@ -190,6 +195,7 @@ def should_fetch_sdkconfig_files(platform_targets):
|
||||
|
||||
def fetch_sdkconfig_files(platform_targets):
|
||||
for platform in platform_targets:
|
||||
if not platform.startswith("posix"):
|
||||
sdkconfig_filename = f"sdkconfig.app.{platform}"
|
||||
target_path = os.path.join(ttbuild_path, sdkconfig_filename)
|
||||
if not download_file(f"{ttbuild_cdn}/sdk/{sdkconfig_filename}", target_path):
|
||||
@@ -199,8 +205,8 @@ def fetch_sdkconfig_files(platform_targets):
|
||||
|
||||
#region Validation
|
||||
|
||||
def validate_environment():
|
||||
if os.environ.get("IDF_PATH") is None:
|
||||
def validate_environment(platforms):
|
||||
if any(not platform.startswith("posix") for platform in platforms) and os.environ.get("IDF_PATH") is None:
|
||||
if sys.platform == "win32":
|
||||
exit_with_error("Cannot find the Espressif IDF SDK. Ensure it is installed and that it is activated via %IDF_PATH%\\export.ps1")
|
||||
else:
|
||||
@@ -311,6 +317,44 @@ def sdk_download_all(version, platforms):
|
||||
|
||||
#endregion SDK download
|
||||
|
||||
#region CMakeLists scaffolding
|
||||
|
||||
# Bump whenever CMAKELISTS_TEMPLATE below changes, so existing apps' generated CMakeLists.txt get
|
||||
# regenerated on their next build instead of silently going stale.
|
||||
CMAKELISTS_VERSION = 1
|
||||
|
||||
CMAKELISTS_TEMPLATE = """# tactility-cmakelists-version: %(version)d
|
||||
cmake_minimum_required(VERSION 3.20)
|
||||
|
||||
if (NOT DEFINED ENV{TACTILITY_SDK_PATH})
|
||||
message(FATAL_ERROR "TACTILITY_SDK_PATH environment variable is not set")
|
||||
endif()
|
||||
|
||||
get_filename_component(TACTILITY_SDK_PATH "$ENV{TACTILITY_SDK_PATH}" ABSOLUTE)
|
||||
include("${TACTILITY_SDK_PATH}/TactilitySDK.cmake")
|
||||
|
||||
tactility_project_pre(%(app_id)s)
|
||||
project(%(app_id)s)
|
||||
tactility_project_post(%(app_id)s)
|
||||
"""
|
||||
|
||||
def cmakelists_version_marker():
|
||||
return f"# tactility-cmakelists-version: {CMAKELISTS_VERSION}"
|
||||
|
||||
def ensure_cmakelists_up_to_date(manifest):
|
||||
marker = cmakelists_version_marker()
|
||||
if os.path.exists("CMakeLists.txt"):
|
||||
with open("CMakeLists.txt", "r") as file:
|
||||
first_line = file.readline().rstrip("\n")
|
||||
if first_line == marker:
|
||||
return
|
||||
print(f"Updating CMakeLists.txt to {marker}")
|
||||
content = CMAKELISTS_TEMPLATE % {"version": CMAKELISTS_VERSION, "app_id": manifest["app.id"]}
|
||||
with open("CMakeLists.txt", "w") as file:
|
||||
file.write(content)
|
||||
|
||||
#endregion CMakeLists scaffolding
|
||||
|
||||
#region Building
|
||||
|
||||
def get_cmake_path(platform):
|
||||
@@ -318,18 +362,65 @@ def get_cmake_path(platform):
|
||||
|
||||
def find_elf_file(platform):
|
||||
cmake_dir = get_cmake_path(platform)
|
||||
if os.path.exists(cmake_dir):
|
||||
if not os.path.exists(cmake_dir):
|
||||
return None
|
||||
# POSIX apps are dlopen()ed shared objects (app-posix-module), not idf.py/elf_loader
|
||||
# relocatable images, so they land as a plain ".so" instead of "*.app.elf".
|
||||
suffix = ".so" if platform.startswith("posix") else ".app.elf"
|
||||
for file in os.listdir(cmake_dir):
|
||||
if file.endswith(".app.elf"):
|
||||
if file.endswith(suffix):
|
||||
return os.path.join(cmake_dir, file)
|
||||
return None
|
||||
|
||||
def get_posix_build_env(sdk_dir):
|
||||
# A dev shell may already have ESP-IDF's export.sh sourced; strip it so the SDK's plain-CMake
|
||||
# top-level CMakeLists.txt takes the POSIX branch instead of the idf.py one.
|
||||
env = os.environ.copy()
|
||||
env.pop("ESP_IDF_VERSION", None)
|
||||
env.pop("IDF_PATH", None)
|
||||
env["TACTILITY_SDK_PATH"] = sdk_dir
|
||||
return env
|
||||
|
||||
def build_posix(version, platform, skip_build):
|
||||
sdk_dir = get_sdk_dir(version, platform)
|
||||
if verbose:
|
||||
print(f"Using SDK at {sdk_dir}")
|
||||
if skip_build:
|
||||
return True
|
||||
env = get_posix_build_env(sdk_dir)
|
||||
cmake_path = get_cmake_path(platform)
|
||||
print_status_busy(f"Building {platform}")
|
||||
configure_command = ["cmake", "-S", ".", "-B", cmake_path, "-G", "Ninja"]
|
||||
if verbose:
|
||||
print(f"Running command: {' '.join(configure_command)}")
|
||||
configure_result = subprocess.run(configure_command, env=env, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
|
||||
if configure_result.returncode != 0:
|
||||
print(configure_result.stdout.decode("UTF-8"), end="")
|
||||
print_status_error(f"Configuring {platform}")
|
||||
return False
|
||||
build_command = ["cmake", "--build", cmake_path]
|
||||
if verbose:
|
||||
print(f"Running command: {' '.join(build_command)}")
|
||||
with subprocess.Popen(build_command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, env=env) as process:
|
||||
build_output = wait_for_process(process)
|
||||
if process.returncode == 0:
|
||||
print_status_success(f"Building {platform}")
|
||||
return True
|
||||
else:
|
||||
for line in build_output:
|
||||
print(line, end="")
|
||||
print_status_error(f"Building {platform}")
|
||||
return False
|
||||
|
||||
def build_all(version, platforms, skip_build):
|
||||
for platform in platforms:
|
||||
if platform.startswith("posix"):
|
||||
if not build_posix(version, platform, skip_build):
|
||||
return False
|
||||
# First build command must be "idf.py build", otherwise it fails to execute "idf.py elf"
|
||||
# We check if the ELF file exists and run the correct command
|
||||
# This can lead to code caching issues, so sometimes a clean build is required
|
||||
if find_elf_file(platform) is None:
|
||||
elif find_elf_file(platform) is None:
|
||||
if not build_first(version, platform, skip_build):
|
||||
return False
|
||||
else:
|
||||
@@ -368,6 +459,7 @@ def build_first(version, platform, skip_build):
|
||||
print(f"Using SDK at {sdk_dir}")
|
||||
os.environ["TACTILITY_SDK_PATH"] = sdk_dir
|
||||
sdkconfig_path = os.path.join(ttbuild_path, f"sdkconfig.app.{platform}")
|
||||
if not platform.startswith("posix"):
|
||||
shutil.copy(sdkconfig_path, "sdkconfig")
|
||||
elf_path = find_elf_file(platform)
|
||||
# Remove previous elf file: re-creation of the file is used to measure if the build succeeded,
|
||||
@@ -444,7 +536,10 @@ def package_intermediate_binaries(target_path, platforms):
|
||||
if elf_path is None:
|
||||
print_error(f"ELF file not found for {platform}")
|
||||
return False
|
||||
shutil.copy(elf_path, os.path.join(elf_dir, f"{platform}.elf"))
|
||||
# app-posix-module's loader resolves an installed app to "elf/posix-<arch>.so", matching
|
||||
# its own compile-time architecture, not "*.elf".
|
||||
extension = ".so" if platform.startswith("posix") else ".elf"
|
||||
shutil.copy(elf_path, os.path.join(elf_dir, f"{platform}{extension}"))
|
||||
return True
|
||||
|
||||
def package_intermediate_assets(target_path):
|
||||
@@ -463,12 +558,10 @@ def package_intermediate(platforms):
|
||||
package_intermediate_assets(target_path)
|
||||
return True
|
||||
|
||||
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_name(manifest):
|
||||
return os.path.join("build", f"{manifest['app.id']}.app")
|
||||
|
||||
def package_all(platforms):
|
||||
def package_all(manifest, platforms):
|
||||
status = f"Building package with {platforms}"
|
||||
print_status_busy(status)
|
||||
if not package_intermediate(platforms):
|
||||
@@ -476,7 +569,7 @@ def package_all(platforms):
|
||||
return False
|
||||
# Create build/something.app
|
||||
try:
|
||||
tar_path = package_name(platforms)
|
||||
tar_path = package_name(manifest)
|
||||
with tarfile.open(tar_path, mode="w", format=tarfile.USTAR_FORMAT) as tar:
|
||||
tar.add(os.path.join("build", "package-intermediate"), arcname="")
|
||||
print_status_success(status)
|
||||
@@ -492,9 +585,10 @@ def setup_environment():
|
||||
os.makedirs(ttbuild_path, exist_ok=True)
|
||||
|
||||
def build_action(manifest, platform_arg, skip_build):
|
||||
# Environment validation
|
||||
validate_environment()
|
||||
ensure_cmakelists_up_to_date(manifest)
|
||||
platforms_to_build = get_manifest_target_platforms(manifest, platform_arg)
|
||||
# Environment validation
|
||||
validate_environment(platforms_to_build)
|
||||
|
||||
if use_local_sdk:
|
||||
global local_base_path
|
||||
@@ -515,7 +609,7 @@ def build_action(manifest, platform_arg, skip_build):
|
||||
if not build_all(sdk_version, platforms_to_build, skip_build): # Environment validation
|
||||
return False
|
||||
if not skip_build:
|
||||
if not package_all(platforms_to_build):
|
||||
if not package_all(manifest, platforms_to_build):
|
||||
return False
|
||||
return True
|
||||
|
||||
@@ -570,14 +664,14 @@ def run_action(manifest, ip):
|
||||
except requests.RequestException as e:
|
||||
print_status_error(f"Running request failed: {e}")
|
||||
|
||||
def install_action(ip, platforms):
|
||||
def install_action(manifest, ip, platforms):
|
||||
print_status_busy("Installing")
|
||||
for platform in platforms:
|
||||
elf_path = find_elf_file(platform)
|
||||
if elf_path is None:
|
||||
print_status_error(f"ELF file not built for {platform}")
|
||||
return False
|
||||
package_path = package_name(platforms)
|
||||
package_path = package_name(manifest)
|
||||
# print(f"Installing {package_path} to {ip}")
|
||||
url = get_url(ip, "/app/install")
|
||||
try:
|
||||
@@ -586,7 +680,7 @@ def install_action(ip, platforms):
|
||||
files = {
|
||||
'elf': file
|
||||
}
|
||||
response = requests.put(url, files=files, timeout=http_timeout_seconds)
|
||||
response = requests.put(url, files=files, timeout=install_timeout_seconds)
|
||||
if response.status_code != 200:
|
||||
print_status_error("Install failed")
|
||||
return False
|
||||
@@ -691,7 +785,7 @@ if __name__ == "__main__":
|
||||
if len(sys.argv) >= 4:
|
||||
platform = sys.argv[3]
|
||||
platforms_to_install = [platform]
|
||||
install_action(sys.argv[2], platforms_to_install)
|
||||
install_action(manifest, sys.argv[2], platforms_to_install)
|
||||
elif action_arg == "uninstall":
|
||||
if len(sys.argv) < 3:
|
||||
print_help()
|
||||
@@ -707,7 +801,7 @@ if __name__ == "__main__":
|
||||
platform = sys.argv[3]
|
||||
platforms_to_install = [platform]
|
||||
if build_action(manifest, platform, skip_build):
|
||||
if install_action(sys.argv[2], platforms_to_install):
|
||||
if install_action(manifest, sys.argv[2], platforms_to_install):
|
||||
run_action(manifest, sys.argv[2])
|
||||
else:
|
||||
print_help()
|
||||
|
||||
Reference in New Issue
Block a user