Compare commits

...

11 Commits

Author SHA1 Message Date
Adolfo Reyna 310b955652 Migrate ES3C35P to latest main firmware 2026-09-10 13:31:10 -04:00
Ken Van Hoeylandt 8556103eb1 Implemented multi-binary app packaging (#648) 2026-09-09 20:05:04 +02:00
Shadowtrance 2496dea5c2 M5Stack PaperS3 display driver improved (#647) 2026-09-08 21:57:41 +02:00
Ken Van Hoeylandt 1b16184a72 Multi-platform http server (#646)
- Add server support to `http-module`
- Ensure `DevelopmentService` works on all platforms (including posix)
- Ensure the built-in web server with dashboard works on all platforms (inluding posix). It still has some issues with certain featuers, but the basics work.
2026-09-06 14:16:23 +02:00
Ken Van Hoeylandt a0b2ee7ebc Run executables directly (#645)
- Apps can be launched directly from file paths (including Files app support)
- Added executable detection to identify unsupported or invalid binaries before launch.
- App startup is now streamlined through separate registered-app and direct-execution interfaces.
- Existing app launch points were migrated to the updated startup APIs.
2026-09-04 21:23:08 +02:00
Ken Van Hoeylandt 643cbc3806 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
2026-08-31 22:09:04 +02:00
Shadowtrance d3656bcd3d Fixes and improvements (#642) 2026-08-30 23:10:52 +02:00
Ken Van Hoeylandt 7a81b525ed elf_loader update and fix for remote install (#641) 2026-08-30 12:58:48 +02:00
Ken Van Hoeylandt 6e5e35610b Implement app streams: stdio for apps (#640)
Implement file IO for applications to facilitate text input/output capturing.

Apps can now launch apps and:
- Read their stdio output
- Write to their stdio input
2026-08-30 12:07:40 +02:00
Ken Van Hoeylandt 64fb1a9f52 display backlight and bluetooth reference counting (#639) 2026-08-29 23:37:31 +02:00
Ken Van Hoeylandt 19b11eb9a8 Remove TactilityC and Firmware subprojects (#638)
- Removed TactilityC, moved its symbols to existing modules and several new ones (pthread-module, c-symbols-module, cpp-symbols-module, posix-symbols-module, freertos-module).
- Removed Firmware subproject and moved main() into Tactility subproject.
- Strengthened application archive, path, version, stack-size, and device validation.
- Moved symbol resolution for elf_loader to app-esp32-module.
- Improved `struct Module` declarations and made module and symbol definitions in modules more consistent.
- Removed old http download code from Tactility subproject.
- Rename app-module's source files for consistency.
- Add missing pthread symbols.
- Kernel module symbols are now resolvable on all platforms.
2026-08-29 21:06:43 +02:00
330 changed files with 18787 additions and 7535 deletions
@@ -0,0 +1,41 @@
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
env:
TACTILITY_ARCH: ${{ steps.arch.outputs.value }}
run: |
TACTILITY_SDK_NAME="$(cat version.txt)-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 -a 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
+3 -3
View File
@@ -29,13 +29,13 @@ 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
# 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:
run: |
TACTILITY_SDK_NAME="0.0.0-${{ inputs.arch }}"
TACTILITY_SDK_NAME="$(cat version.txt)-${{ inputs.arch }}"
mkdir -p test_sdk/$TACTILITY_SDK_NAME
cp -r release/TactilitySDK test_sdk/$TACTILITY_SDK_NAME
- name: 'Test Integration'
@@ -43,7 +43,7 @@ runs:
with:
esp_idf_version: v5.5.2
target: ${{ inputs.arch }}
command: export TACTILITY_SDK_PATH=../../test_sdk && cd Tests/SdkIntegration && python tactility.py build ${{ inputs.arch }} --local-sdk
command: export TACTILITY_SDK_PATH=../../test_sdk && cd Tests/SdkIntegration && python tactility.py build -a ${{ inputs.arch }} --local-sdk
- name: 'Upload Artifact'
uses: actions/upload-artifact@v4
with:
+1 -1
View File
@@ -45,7 +45,7 @@ runs:
run: cmake -S ./ -B buildsim
- name: "Build Tests"
shell: bash
run: cmake --build buildsim --target FirmwareSim
run: cmake --build buildsim --target Tactility
- name: 'Release'
shell: bash
run: Buildscripts/release-simulator.sh buildsim release/Simulator-${{ inputs.os_name }}-${{ inputs.platform_name }}
+11 -3
View File
@@ -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'))
+3
View File
@@ -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__
@@ -311,11 +311,39 @@ def test_compile_missing_config():
print("PASSED")
return True
def test_es3c35p_uses_current_runtime_contract():
print("Running test_es3c35p_uses_current_runtime_contract...")
repository_root = os.path.abspath(os.path.join(SCRIPT_DIR, "..", "..", ".."))
device_dir = os.path.join(repository_root, "Devices", "es3c35p")
with open(os.path.join(device_dir, "device.properties")) as f:
properties = f.read()
with open(os.path.join(device_dir, "es3c35p.dts")) as f:
devicetree = f.read()
requirements = [
("apps.launcherAppId=tactility.launcher" in properties, "current launcher app id"),
("hardware.tinyUsb=" not in properties, "no obsolete hardware.tinyUsb property"),
('wifi0 {\n\t\tcompatible = "espressif,esp32-wifi-pinned";\n\t};' in devicetree,
"Wi-Fi enabled for web server and MCP"),
('ble0 {\n\t\tcompatible = "espressif,esp32-ble";\n\t};' in devicetree,
"BLE node matches hardware.bluetooth=true"),
]
missing = [description for condition, description in requirements if not condition]
if missing:
print("FAILED: " + ", ".join(missing))
return False
print("PASSED")
return True
if __name__ == "__main__":
tests = [
test_compile_success,
test_compile_invalid_dts,
test_compile_missing_config,
test_es3c35p_uses_current_runtime_contract,
test_minmax_within_range_succeeds,
test_minmax_below_minimum_fails,
test_minmax_above_maximum_fails,
-3
View File
@@ -1,6 +1,5 @@
idf_component_register(
INCLUDE_DIRS
"Libraries/TactilityC/include"
"Libraries/TactilityKernel/include"
"Libraries/TactilityFreeRtos/Include"
"Libraries/lvgl/include"
@@ -11,13 +10,11 @@ idf_component_register(
)
# Regular and core features
add_prebuilt_library(TactilityC Libraries/TactilityC/binary/libTactilityC.a)
add_prebuilt_library(TactilityKernel Libraries/TactilityKernel/binary/libTactilityKernel.a)
add_prebuilt_library(lvgl Libraries/lvgl/binary/liblvgl.a)
add_prebuilt_library(minmea Libraries/minmea/binary/libminmea.a)
add_prebuilt_library(minitar Libraries/minitar/binary/libminitar.a)
target_link_libraries(${COMPONENT_LIB} INTERFACE TactilityC)
target_link_libraries(${COMPONENT_LIB} INTERFACE TactilityKernel)
target_link_libraries(${COMPONENT_LIB} INTERFACE lvgl)
target_link_libraries(${COMPONENT_LIB} INTERFACE minmea)
@@ -1,35 +0,0 @@
function(tactility_project)
endfunction()
function(_tactility_project)
endfunction()
macro(tactility_project project_name)
set(TACTILITY_SKIP_SPIFFS 1)
include("${TACTILITY_SDK_PATH}/Libraries/elf_loader/elf_loader.cmake")
project_elf($project_name)
file(READ ${TACTILITY_SDK_PATH}/idf-version.txt TACTILITY_SDK_IDF_VERSION)
string(REGEX REPLACE "^([0-9]+\\.[0-9]+).*" "\\1" TACTILITY_SDK_IDF_MAJOR_MINOR "${TACTILITY_SDK_IDF_VERSION}")
string(REGEX REPLACE "^([0-9]+\\.[0-9]+).*" "\\1" CURRENT_IDF_MAJOR_MINOR "$ENV{ESP_IDF_VERSION}")
if (NOT "${CURRENT_IDF_MAJOR_MINOR}" STREQUAL "${TACTILITY_SDK_IDF_MAJOR_MINOR}")
message(FATAL_ERROR "ESP-IDF version of Tactility SDK (${TACTILITY_SDK_IDF_VERSION}) does not match current ESP-IDF version ($ENV{ESP_IDF_VERSION})")
endif()
set(EXTRA_COMPONENT_DIRS
"${TACTILITY_SDK_PATH}/Libraries/TactilityFreeRtos"
"${TACTILITY_SDK_PATH}/Modules"
)
set(COMPONENTS
TactilityFreeRtos
app-module
crypt-module
gps-module
lvgl-module
lvgl-window-manager-module
service-module
)
endmacro()
@@ -0,0 +1,62 @@
function(tactility_project)
endfunction()
function(_tactility_project)
endfunction()
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
# a RISC-V app's crashes, which GCC does not guarantee without this flag. The firmware sets the
# same flag for its own code, but a crash usually happens in app code, built separately here.
# Gated to RISC-V since Xtensa never reads s0 this way. idf_build_set_property(), not
# add_compile_options(): the app's code compiles as an idf_component_register() component
# (Apps/*/main/CMakeLists.txt), which reads ESP-IDF's own COMPILE_OPTIONS build property rather
# than plain directory-scoped flags. project_elf() below uses the same property for its own
# flags for the same reason.
if(CONFIG_IDF_TARGET_ARCH_RISCV)
idf_build_set_property(COMPILE_OPTIONS "-fno-omit-frame-pointer" APPEND)
endif()
include("${TACTILITY_SDK_PATH}/Libraries/elf_loader/elf_loader.cmake")
project_elf($project_name)
file(READ ${TACTILITY_SDK_PATH}/idf-version.txt TACTILITY_SDK_IDF_VERSION)
string(REGEX REPLACE "^([0-9]+\\.[0-9]+).*" "\\1" TACTILITY_SDK_IDF_MAJOR_MINOR "${TACTILITY_SDK_IDF_VERSION}")
string(REGEX REPLACE "^([0-9]+\\.[0-9]+).*" "\\1" CURRENT_IDF_MAJOR_MINOR "$ENV{ESP_IDF_VERSION}")
if (NOT "${CURRENT_IDF_MAJOR_MINOR}" STREQUAL "${TACTILITY_SDK_IDF_MAJOR_MINOR}")
message(FATAL_ERROR "ESP-IDF version of Tactility SDK (${TACTILITY_SDK_IDF_VERSION}) does not match current ESP-IDF version ($ENV{ESP_IDF_VERSION})")
endif()
set(EXTRA_COMPONENT_DIRS
"${TACTILITY_SDK_PATH}/Libraries/TactilityFreeRtos"
"${TACTILITY_SDK_PATH}/Modules"
)
set(COMPONENTS
TactilityFreeRtos
app-module
crypt-module
gps-module
lvgl-module
lvgl-window-manager-module
service-module
)
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()
-1
View File
@@ -5,7 +5,6 @@ import time
def build(device: str) -> bool:
print(f"Building {device}...")
shutil.rmtree(os.path.join('Firmware', 'Generated'), ignore_errors=True)
result = subprocess.run(['python', 'device.py', device], capture_output=True, text=True)
if result.returncode != 0:
print(f"Failed to select device {device}")
+87
View File
@@ -0,0 +1,87 @@
#!/usr/bin/env python3
"""Convert an image to an uncompressed LVGL RGB565 launcher background."""
import argparse
import shutil
import struct
import subprocess
from pathlib import Path
LV_IMAGE_HEADER_MAGIC = 0x19
LV_COLOR_FORMAT_RGB565 = 0x12
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(
description=(
"Create an uncompressed LVGL .bin image for "
"/sdcard/tactility/launcher/background.bin. Use a square image sized "
"to the display's longest edge to support both orientations without scaling "
"(for example, 320x320 for a 320x240 display)."
)
)
parser.add_argument("input", type=Path, help="Source image")
parser.add_argument("output", type=Path, help="Destination .bin file")
parser.add_argument("--width", type=int, required=True, help="Output width")
parser.add_argument("--height", type=int, required=True, help="Output height")
return parser.parse_args()
def main() -> None:
args = parse_args()
if args.width <= 0 or args.width > 65535 or args.height <= 0 or args.height > 65535:
raise SystemExit("width and height must be between 1 and 65535")
magick = shutil.which("magick")
if magick is None:
raise SystemExit("ImageMagick is required (the 'magick' command was not found)")
command = [
magick,
str(args.input),
"-resize",
f"{args.width}x{args.height}^",
"-gravity",
"center",
"-extent",
f"{args.width}x{args.height}",
"-depth",
"8",
"rgb:-",
]
rgb888 = subprocess.run(command, check=True, stdout=subprocess.PIPE).stdout
expected_size = args.width * args.height * 3
if len(rgb888) != expected_size:
raise SystemExit(f"unexpected ImageMagick output: {len(rgb888)} bytes, expected {expected_size}")
rgb565 = bytearray(args.width * args.height * 2)
for source_offset in range(0, len(rgb888), 3):
r, g, b = rgb888[source_offset : source_offset + 3]
pixel = ((r >> 3) << 11) | ((g >> 2) << 5) | (b >> 3)
destination_offset = (source_offset // 3) * 2
struct.pack_into("<H", rgb565, destination_offset, pixel)
stride = args.width * 2
header = struct.pack(
"<BBHHHHH",
LV_IMAGE_HEADER_MAGIC,
LV_COLOR_FORMAT_RGB565,
0,
args.width,
args.height,
stride,
0,
)
args.output.parent.mkdir(parents=True, exist_ok=True)
args.output.write_bytes(header + rgb565)
print(
f"Wrote {args.output} ({args.width}x{args.height}, "
f"{len(header) + len(rgb565)} bytes, uncompressed RGB565)"
)
if __name__ == "__main__":
main()
+11 -3
View File
@@ -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()
+31 -19
View File
@@ -1,6 +1,7 @@
#!/usr/bin/env python3
import os
import platform
import shutil
import subprocess
import sys
@@ -17,42 +18,53 @@ 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])
if result.returncode != 0:
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,66 +1,16 @@
#!/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)
_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)
# 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 get_driver_mappings(driver_name):
return [
{'src': f'Drivers/{driver_name}/include/**', 'dst': f'Drivers/{driver_name}/include/'},
@@ -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,25 @@ 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", "")
if not esp_idf_version:
print("Error: ESP_IDF_VERSION environment variable is not set")
sys.exit(1)
target_path = os.path.abspath(sys.argv[1])
@@ -131,11 +74,6 @@ def main():
# Mapping logic
mappings = [
{'src': 'version.txt', 'dst': ''},
# TactilityC
{'src': 'build/esp-idf/TactilityC/libTactilityC.a', 'dst': 'Libraries/TactilityC/binary/'},
{'src': 'TactilityC/Include/*', 'dst': 'Libraries/TactilityC/include/'},
{'src': 'TactilityC/CMakeLists.txt', 'dst': 'Libraries/TactilityC/'},
{'src': 'TactilityC/LICENSE*.*', 'dst': 'Libraries/TactilityC/'},
# TactilityFreeRtos
{'src': 'TactilityFreeRtos/Include/**', 'dst': 'Libraries/TactilityFreeRtos/Include/'},
{'src': 'TactilityFreeRtos/CMakeLists.txt', 'dst': 'Libraries/TactilityFreeRtos/'},
@@ -153,8 +91,9 @@ def main():
{'src': 'Libraries/lvgl/src/lv_conf_kconfig.h', 'dst': 'Libraries/lvgl/include/lv_conf.h'},
{'src': 'Libraries/lvgl/src/**/*.h', 'dst': 'Libraries/lvgl/include/src/'},
# elf_loader
{'src': 'Libraries/elf_loader/elf_loader.cmake', 'dst': 'Libraries/elf_loader/'},
{'src': 'Libraries/elf_loader/license.txt', 'dst': 'Libraries/elf_loader/'},
{'src': 'managed_components/espressif__elf_loader/*.cmake', 'dst': 'Libraries/elf_loader/'},
{'src': 'managed_components/espressif__elf_loader/*.lf', 'dst': 'Libraries/elf_loader/'},
{'src': 'managed_components/espressif__elf_loader/license.txt', 'dst': 'Libraries/elf_loader/'},
# minitar
{'src': 'build/esp-idf/minitar/libminitar.a', 'dst': 'Libraries/minitar/binary/'},
{'src': 'Libraries/minitar/minitar/minitar.h', 'dst': 'Libraries/minitar/include/'},
@@ -168,23 +107,18 @@ 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
esp_idf_version = os.environ.get("ESP_IDF_VERSION", "")
with open(os.path.join(target_path, "idf-version.txt"), "a") as f:
f.write(esp_idf_version)
+7
View File
@@ -0,0 +1,7 @@
app-module
crypt-module
gps-module
http-module
lvgl-module
lvgl-window-manager-module
service-module
+101
View File
@@ -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()
+91
View File
@@ -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'))
+5 -5
View File
@@ -9,9 +9,9 @@
build_path=$1
target_path=$2
mkdir -p $target_path
mkdir -p "$target_path"
cp version.txt $target_path
cp $build_path/Firmware/FirmwareSim $target_path/
cp -r Data/data $target_path/
cp -r Data/system $target_path/
cp version.txt "$target_path"
cp "$build_path/Tactility/Tactility" "$target_path/"
cp -r Data/data "$target_path/"
cp -r Data/system "$target_path/"
+38 -15
View File
@@ -33,19 +33,22 @@ if (DEFINED ENV{ESP_IDF_VERSION})
message("Using ESP-IDF ${Cyan}v$ENV{ESP_IDF_VERSION}${ColorReset}")
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
set(COMPONENTS Firmware)
set(COMPONENTS Tactility)
set(EXTRA_COMPONENT_DIRS
"Firmware"
# Tactility must be discovered first: its CMakeLists.txt calls init_tactility_globals(),
# which other components' CMakeLists.txt (e.g. lvgl-module) rely on having already run to
# read back TACTILITY_DEVICE_ID/TACTILITY_DEVICE_PROJECT via get_property(). ESP-IDF's
# requirements-discovery pass processes EXTRA_COMPONENT_DIRS in an isolated sub-process, in
# the order listed here, so this must stay first now that Firmware no longer exists as a
# separate component.
"Tactility"
"Devices/${TACTILITY_DEVICE_PROJECT}"
"Drivers"
"Modules"
"Platforms/platform-esp32"
"TactilityKernel"
"TactilityKernelCpp"
"Tactility"
"TactilityC"
"TactilityFreeRtos"
"Libraries/elf_loader"
"Libraries/esp_epaper"
"Libraries/lv_screenshot"
"Libraries/minitar"
@@ -55,10 +58,13 @@ if (DEFINED ENV{ESP_IDF_VERSION})
set(EXCLUDE_COMPONENTS "Simulator")
# Panic handler wrapping is only available on Xtensa architecture
if (CONFIG_IDF_TARGET_ARCH_XTENSA)
idf_build_set_property(LINK_OPTIONS "-Wl,--wrap=esp_panic_handler" APPEND)
endif ()
# panic_info_t is architecture-independent (esp_private/panic_internal.h) so this wrap applies
# to every target; PanicHandler.cpp branches internally per architecture.
idf_build_set_property(LINK_OPTIONS "-Wl,--wrap=esp_panic_handler" APPEND)
idf_build_set_property(LINK_OPTIONS "-Wl,--wrap=read" APPEND)
idf_build_set_property(LINK_OPTIONS "-Wl,--wrap=write" APPEND)
idf_build_set_property(LINK_OPTIONS "-Wl,--wrap=close" APPEND)
idf_build_set_property(LINK_OPTIONS "-Wl,--wrap=lv_button_create" APPEND)
idf_build_set_property(LINK_OPTIONS "-Wl,--wrap=lv_dropdown_create" APPEND)
@@ -71,9 +77,7 @@ if (DEFINED ENV{ESP_IDF_VERSION})
else ()
message("Building for sim target")
# Devices/simulator/Source/Simulator.cpp always defines its own hardwareConfiguration; without
# this, Firmware/Source/Main.cpp's #else branch also defines an empty one (its ESP32-only
# fallback for devices migrated off the deprecated HAL), causing a duplicate-definition link error.
# eps-idf generates these from Kconfig, but posix build isn't set up with Kconfig.
add_compile_definitions(CONFIG_TT_DEVICE_ID="simulator")
add_compile_definitions(CONFIG_TT_DEVICE_NAME="Simulator")
add_compile_definitions(CONFIG_TT_DEVICE_VENDOR="")
@@ -85,6 +89,21 @@ endif ()
project(Tactility)
if (DEFINED ENV{ESP_IDF_VERSION})
# PanicHandler.cpp's RISC-V callstack walker requires s0 to stay a frame pointer, which GCC
# does not guarantee without this flag. CONFIG_ESP_SYSTEM_USE_FRAME_POINTER does not add it
# (it only selects which ESP-IDF backtrace-printing function gets compiled), and can't be used
# instead: the bootloader shares this project's sdkconfig with no per-subproject override, and
# enabling it there overflows the bootloader's fixed partition budget. Setting the flag here via
# idf_build_set_property() only affects this project's own configure, not the bootloader's
# separate one, so it's naturally excluded. Gated to RISC-V since Xtensa never reads s0 this way.
# Must run after project(Tactility) above - project() initializes default build specifications
# that would otherwise overwrite this.
if(CONFIG_IDF_TARGET_ARCH_RISCV)
idf_build_set_property(COMPILE_OPTIONS "-fno-omit-frame-pointer" APPEND)
endif()
endif ()
# Defined as regular project for PC and component for ESP
if (NOT DEFINED ENV{ESP_IDF_VERSION})
add_subdirectory(Tactility)
@@ -99,11 +118,18 @@ if (NOT DEFINED ENV{ESP_IDF_VERSION})
add_subdirectory(Libraries/minitar)
add_subdirectory(Libraries/minmea)
add_subdirectory(Modules/lvgl-module)
add_subdirectory(Modules/c-symbols-module)
add_subdirectory(Modules/cpp-symbols-module)
add_subdirectory(Modules/crypt-module)
add_subdirectory(Modules/freertos-module)
add_subdirectory(Modules/gps-module)
add_subdirectory(Modules/http-module)
add_subdirectory(Modules/mbedtls-module)
add_subdirectory(Modules/posix-symbols-module)
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)
@@ -130,9 +156,6 @@ if (NOT DEFINED ENV{ESP_IDF_VERSION})
add_subdirectory(Libraries/lvgl) # Added as idf component for ESP and as library for other targets
target_link_libraries(lvgl PRIVATE SDL2-static)
# Sim app
add_subdirectory(Firmware)
# Tests
add_subdirectory(Tests)
Binary file not shown.

After

Width:  |  Height:  |  Size: 298 KiB

+4
View File
@@ -0,0 +1,4 @@
idf_component_register(
SRCS "source/module.cpp"
REQUIRES TactilityKernel
)
+27
View File
@@ -0,0 +1,27 @@
general.vendor=LCDWIKI/Hosyond
general.name=ES3C35P
apps.launcherAppId=tactility.launcher
hardware.target=ESP32S3
hardware.flashSize=16MB
hardware.spiRam=true
hardware.spiRamMode=OCT
hardware.spiRamSpeed=120M
hardware.esptoolFlashFreq=120M
hardware.bluetooth=true
display.size=3.5"
display.shape=rectangle
display.dpi=165
lvgl.colorDepth=16
storage.userDataLocation=SD
dependencies.useDeprecatedHal=false
# Launcher clock and full-screen wallpaper
sdkconfig.CONFIG_LV_FONT_MONTSERRAT_48=y
sdkconfig.CONFIG_LV_CACHE_DEF_SIZE=1048576
sdkconfig.CONFIG_LV_IMAGE_HEADER_CACHE_DEF_CNT=16
+6
View File
@@ -0,0 +1,6 @@
dependencies:
- Platforms/platform-esp32
- Drivers/st77922-module
- Drivers/es8311-module
- Drivers/audio-stream-module
dts: es3c35p.dts
+141
View File
@@ -0,0 +1,141 @@
/dts-v1/;
#include <tactility/bindings/root.h>
#include <tactility/bindings/esp32_adc_oneshot.h>
#include <tactility/bindings/esp32_ble.h>
#include <tactility/bindings/esp32_gpio.h>
#include <tactility/bindings/esp32_i2c.h>
#include <tactility/bindings/esp32_i2s.h>
#include <tactility/bindings/esp32_pwm_ledc.h>
#include <tactility/bindings/esp32_sdmmc.h>
#include <tactility/bindings/esp32_spi.h>
#include <tactility/bindings/esp32_wifi_pinned.h>
#include <tactility/bindings/battery_sense.h>
#include <tactility/bindings/gpio_hog.h>
#include <tactility/bindings/pwm_backlight.h>
#include <bindings/st77922.h>
#include <bindings/st77922_touch.h>
#include <bindings/es8311.h>
/ {
compatible = "root";
model = "LCDWIKI/Hosyond ES3C35P";
wifi0 {
compatible = "espressif,esp32-wifi-pinned";
};
ble0 {
compatible = "espressif,esp32-ble";
};
gpio0 {
compatible = "espressif,esp32-gpio";
gpio-count = <49>;
};
/* FM8002E speaker amplifier enable, active-low on GPIO1. */
amp_enable {
compatible = "gpio-hog";
pin = <&gpio0 1 GPIO_FLAG_NONE>;
mode = <GPIO_HOG_MODE_OUTPUT_LOW>;
};
adc0 {
compatible = "espressif,esp32-adc-oneshot";
unit-id = <ADC_UNIT_1>;
clk-src = <ADC_RTC_CLK_SRC_DEFAULT>;
channels = <ADC_CHANNEL_7 ADC_ATTEN_DB_12 ADC_BITWIDTH_DEFAULT>;
};
/* BAT+ through a 200K/200K divider into GPIO8 / ADC1_CH7. */
battery-sense {
compatible = "battery-sense";
io-channel = <&adc0 0>;
reference-voltage-mv = <3300>;
multiplier = <2000>;
};
i2s0 {
compatible = "espressif,esp32-i2s";
port = <I2S_NUM_0>;
pin-bclk = <&gpio0 18 GPIO_FLAG_NONE>;
pin-ws = <&gpio0 21 GPIO_FLAG_NONE>;
pin-data-out = <&gpio0 15 GPIO_FLAG_NONE>;
pin-data-in = <&gpio0 16 GPIO_FLAG_NONE>;
pin-mclk = <&gpio0 17 GPIO_FLAG_NONE>;
};
i2c0 {
compatible = "espressif,esp32-i2c";
port = <I2C_NUM_0>;
clock-frequency = <400000>;
pin-sda = <&gpio0 38 GPIO_FLAG_NONE>;
pin-scl = <&gpio0 39 GPIO_FLAG_NONE>;
touch@55 {
compatible = "sitronix,st77922-touch";
reg = <0x55>;
x-max = <320>;
y-max = <480>;
pin-reset = <&gpio0 48 GPIO_FLAG_NONE>;
pin-interrupt = <&gpio0 47 GPIO_FLAG_NONE>;
};
es8311: es8311@18 {
compatible = "everest,es8311";
reg = <0x18>;
i2s = <&i2s0>;
};
};
display_backlight_pwm {
compatible = "espressif,esp32-pwm-ledc";
pin = <&gpio0 41 GPIO_FLAG_NONE>;
period-ns = <200000>;
ledc-timer = <0>;
ledc-channel = <0>;
};
display_backlight {
compatible = "pwm-backlight";
status = "disabled";
pwm = <&display_backlight_pwm>;
};
spi0 {
compatible = "espressif,esp32-spi";
host = <SPI2_HOST>;
pin-sclk = <&gpio0 12 GPIO_FLAG_NONE>;
pin-mosi = <&gpio0 11 GPIO_FLAG_NONE>;
pin-miso = <&gpio0 13 GPIO_FLAG_NONE>;
pin-wp = <&gpio0 14 GPIO_FLAG_NONE>;
pin-hd = <&gpio0 9 GPIO_FLAG_NONE>;
cs-gpios = <&gpio0 10 GPIO_FLAG_NONE>;
/* Accommodate the driver's 1/10-frame DMA staging transfers, matching
* the vendor LVGL port's full-frame refresh path. */
max-transfer-size = <65536>;
display@0 {
compatible = "sitronix,st77922";
horizontal-resolution = <320>;
vertical-resolution = <480>;
/* 80 MHz works in the vendor demo, but produces visible QSPI corruption on
* some modules/cables. The component's 40 MHz default is reliably clean. */
pixel-clock-hz = <40000000>;
backlight = <&display_backlight>;
};
};
sdmmc0 {
compatible = "espressif,esp32-sdmmc";
pin-clk = <&gpio0 5 GPIO_FLAG_NONE>;
pin-cmd = <&gpio0 4 GPIO_FLAG_NONE>;
pin-d0 = <&gpio0 6 GPIO_FLAG_NONE>;
pin-d1 = <&gpio0 7 GPIO_FLAG_NONE>;
pin-d2 = <&gpio0 2 GPIO_FLAG_NONE>;
pin-d3 = <&gpio0 3 GPIO_FLAG_NONE>;
slot = <SDMMC_HOST_SLOT_1>;
bus-width = <4>;
};
};
+9
View File
@@ -0,0 +1,9 @@
#include <tactility/module.h>
extern "C" {
Module es3c35p_module = {
.name = "es3c35p"
};
}
@@ -10,10 +10,13 @@ properties:
type: int
default: 20
description: Ambient temperature in °C, used for waveform timing compensation
draw-mode:
quality-draw-mode:
type: int
default: MODE_DU
description: Default EpdDrawMode waveform used for screen updates (e.g. MODE_DU, MODE_GC16)
default: MODE_GC16
description: >
EpdDrawMode waveform used for full-quality refreshes (e.g. MODE_GC16, MODE_GL16).
Fast partial updates always use MODE_DU internally and are not configurable - see
driver comments.
rotation:
type: int
default: EPD_ROT_PORTRAIT
+1 -1
View File
@@ -7,7 +7,7 @@ apps.launcherAppId=tactility.launcher
hardware.target=esp32s3
hardware.flashSize=16MB
hardware.spiRam=true
hardware.spiRamMode=OPI
hardware.spiRamMode=OCT
hardware.spiRamSpeed=80M
hardware.esptoolFlashFreq=80M
hardware.tinyUsbMsc=true
@@ -0,0 +1,177 @@
// SPDX-License-Identifier: Apache-2.0
/**
* Board definition for M5Stack PaperS3.
*
* Kept out-of-tree (see epd_board_m5papers3.h) instead of forking epdiy: this file only
* uses epdiy's public API, so it builds as an ordinary consumer of the upstream component.
*
* Pin mapping from M5GFX source code (authoritative reference):
* https://github.com/m5stack/M5GFX/blob/master/src/M5GFX.cpp
*
* Data bus: DB0-DB7 on GPIO 6,14,7,12,9,11,8,10
* Control: STH=13, LEH=15, STV=17, CKV=18, CKH=16
* Power: PWR=46, OE=45
*/
#include "epd_board_m5papers3.h"
#include <stdint.h>
#include "epdiy.h"
#include <output_lcd/lcd_driver.h>
#include "esp_log.h"
#include <driver/gpio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#ifndef CONFIG_IDF_TARGET_ESP32S3
#error "M5Paper S3 board only supports ESP32-S3"
#endif
static const char* TAG = "m5paper_s3";
/* Data Lines - from M5GFX source */
#define D0 GPIO_NUM_6
#define D1 GPIO_NUM_14
#define D2 GPIO_NUM_7
#define D3 GPIO_NUM_12
#define D4 GPIO_NUM_9
#define D5 GPIO_NUM_11
#define D6 GPIO_NUM_8
#define D7 GPIO_NUM_10
/* Control Lines - from M5GFX source */
#define STH GPIO_NUM_13 /* Start pulse horizontal (active low) */
#define LEH GPIO_NUM_15 /* Latch enable horizontal */
#define STV GPIO_NUM_17 /* Start vertical */
#define CKV GPIO_NUM_18 /* Clock vertical */
#define CKH GPIO_NUM_16 /* Clock horizontal - LCD peripheral clock output */
/* Power control - from M5GFX source */
#define PWR_PIN GPIO_NUM_46 /* Main power enable */
#define OE_PIN GPIO_NUM_45 /* Output enable */
static lcd_bus_config_t lcd_config = {
.clock = CKH,
.ckv = CKV,
.leh = LEH,
.start_pulse = STH,
.stv = STV,
.data[0] = D0,
.data[1] = D1,
.data[2] = D2,
.data[3] = D3,
.data[4] = D4,
.data[5] = D5,
.data[6] = D6,
.data[7] = D7,
};
static void epd_board_init(uint32_t epd_row_width, const EpdInitConfig* init_config) {
(void)init_config;
ESP_LOGI(TAG, "Initializing M5Paper S3 board");
/* Configure power pin - start with power off */
gpio_reset_pin(PWR_PIN);
gpio_set_direction(PWR_PIN, GPIO_MODE_OUTPUT);
gpio_set_level(PWR_PIN, 0);
/* Configure output enable pin - active high */
gpio_reset_pin(OE_PIN);
gpio_set_direction(OE_PIN, GPIO_MODE_OUTPUT);
gpio_set_level(OE_PIN, 0);
const EpdDisplay_t* display = epd_get_display();
LcdEpdConfig_t config = {
.pixel_clock = display->bus_speed * 1000 * 1000,
.ckv_high_time = 60,
.line_front_porch = 4,
.le_high_time = 4,
.bus_width = display->bus_width,
.bus = lcd_config,
};
epd_lcd_init(&config, display->width, display->height);
ESP_LOGI(TAG, "Board initialized: %dx%d @ %dMHz",
display->width, display->height, display->bus_speed);
}
static void epd_board_deinit() {
ESP_LOGI(TAG, "Deinitializing M5Paper S3 board");
/* Disable output first */
gpio_set_level(OE_PIN, 0);
/* Power off display */
gpio_set_level(PWR_PIN, 0);
epd_lcd_deinit();
}
static void epd_board_set_ctrl(epd_ctrl_state_t* state, const epd_ctrl_state_t* const mask) {
/* Handle output enable changes */
if (mask->ep_output_enable) {
gpio_set_level(OE_PIN, state->ep_output_enable ? 1 : 0);
}
}
static void epd_board_poweron(epd_ctrl_state_t* state) {
ESP_LOGI(TAG, "Powering on display");
/* Enable main power first */
gpio_set_level(PWR_PIN, 1);
/* Wait for power to stabilize */
vTaskDelay(pdMS_TO_TICKS(100));
/* Enable output */
gpio_set_level(OE_PIN, 1);
/* Update state */
state->ep_stv = true;
state->ep_mode = false;
state->ep_output_enable = true;
state->ep_sth = true;
}
static void epd_board_poweroff(epd_ctrl_state_t* state) {
ESP_LOGI(TAG, "Powering off display");
/* Disable output first */
gpio_set_level(OE_PIN, 0);
state->ep_stv = false;
state->ep_output_enable = false;
state->ep_mode = false;
state->ep_sth = false;
/* Small delay before cutting power */
vTaskDelay(pdMS_TO_TICKS(10));
/* Cut main power */
gpio_set_level(PWR_PIN, 0);
}
static float epd_board_ambient_temperature() {
/* TODO: Could read from BMI270 temperature sensor */
return 20.0f;
}
const EpdBoardDefinition epd_board_m5papers3 = {
.init = epd_board_init,
.deinit = epd_board_deinit,
.set_ctrl = epd_board_set_ctrl,
.poweron = epd_board_poweron,
.poweroff = epd_board_poweroff,
.get_temperature = epd_board_ambient_temperature,
// No hardware VCOM control path on this board yet - a non-null callback that doesn't touch
// hardware would let epd_set_vcom() complete without changing anything on the panel.
.set_vcom = NULL,
.gpio_set_direction = NULL,
.gpio_read = NULL,
.gpio_write = NULL,
};
@@ -0,0 +1,20 @@
// SPDX-License-Identifier: Apache-2.0
/**
* @file "epd_board_m5papers3.h"
* @brief Board definition for M5Stack PaperS3, kept out-of-tree because upstream epdiy
* (https://github.com/vroland/epdiy) does not support this board.
*/
#pragma once
#include <epd_board.h>
#ifdef __cplusplus
extern "C" {
#endif
extern const EpdBoardDefinition epd_board_m5papers3;
#ifdef __cplusplus
}
#endif
@@ -7,8 +7,10 @@
#include <tactility/error.h>
#include <tactility/log.h>
#include <tactility/module.h>
#include <tactility/time.h>
#include "epd_board_m5papers3.h"
#include <epd_board.h>
#include <epdiy.h>
#include <esp_heap_caps.h>
@@ -19,6 +21,70 @@
#define TAG "Papers3Display"
#define GET_CONFIG(device) (static_cast<const Papers3DisplayConfig*>((device)->config))
// Fast partial updates are always MODE_DU (strict black/white); config->quality_draw_mode is
// only used for the periodic full-quality pass.
static constexpr EpdDrawMode FAST_DRAW_MODE = MODE_DU;
// A partial update covering at least this fraction of the panel is a full-screen content change
// (e.g. an app switch rebuilding the whole window, see lvgl.md) rather than a small widget
// redraw, and is promoted to a quality refresh immediately.
static constexpr float FULL_AREA_QUALITY_THRESHOLD = 0.6f;
// Bounds worst-case ghost accumulation during sustained fast-mode interaction (e.g. scrolling),
// regardless of idle time. LVGL's PARTIAL-mode draw buffer covers vres/10 rows (see
// lvgl-module/source/devices/devices.cpp's buffer_height), so a single full-screen redraw is
// already ~10 tiles - this must clear a full sweep comfortably, or a normal full-screen redraw
// gets promoted to slow GC16 partway through.
static constexpr uint32_t QUALITY_REFRESH_PARTIAL_COUNT = 20;
// Cleans up ghosting left behind after interaction stops, since nothing else triggers a refresh
// once draw_bitmap() calls stop arriving. Matches the M5Stack official demo's timer.
static constexpr uint32_t QUALITY_REFRESH_IDLE_SECONDS = 10;
// LVGL's PARTIAL render mode flushes one draw_bitmap() call per still-unjoined dirty rect, so
// one visual refresh is usually several back-to-back calls, not one; this holds quality mode
// across a sibling rect's near-zero gap so they don't end up on inconsistent modes. Must stay
// well under a GC16 draw's own duration (400ms+), or it also bridges the much larger gap between
// separate real frames and pins a whole multi-frame interaction to GC16.
static constexpr uint32_t QUALITY_HOLD_MS = 50;
// epd_fullclear() (white fill + GC16 draw + 3-cycle black/white flash, see epdiy's
// highlevel.c/render.c) only runs once, at boot (papers3_display_init()), never periodically:
// it wipes the whole panel, and this driver has no way to force LVGL to redraw everything
// afterward - only whatever rect is drawn next gets restored, leaving the rest blank.
// 4x4 ordered (Bayer) dither thresholds, spread evenly across a 0-15 nibble range.
static constexpr uint8_t BAYER_4X4[4][4] = {
{ 0, 8, 2, 10 },
{ 12, 4, 14, 6 },
{ 3, 11, 1, 9 },
{ 15, 7, 13, 5 },
};
// Dithers an 8-bit luminance sample (0x00=black..0xFF=white) down to a 4-bit nibble
// (0x0=black..0xF=white, matching EPDiy's MODE_PACKING_2PPB), spreading the rounding error
// spatially instead of truncating every pixel the same way - this is what turns flat/banded
// output into something that reads as smooth grayscale.
static inline uint8_t dither_to_nibble(uint8_t luminance, int32_t x, int32_t y) {
// BAYER_4X4 is 0-15; scaled by 17 it spans a full 0-255 quantization step (one increment of
// luminance*15), so the dither bias is actually comparable to the rounding it perturbs
// instead of a few percent of one step.
const uint32_t threshold = BAYER_4X4[y & 3][x & 3] * 17U;
const uint32_t level = (static_cast<uint32_t>(luminance) * 15U + threshold) / 255U;
return static_cast<uint8_t>(level > 15U ? 15U : level);
}
// Binary variant for MODE_DU, which only supports pure black/white (see epdiy.h) - dithering
// still applies so a partial-update area doesn't look coarser than the quality pass that
// preceded it.
static inline uint8_t dither_to_bw_nibble(uint8_t luminance, int32_t x, int32_t y) {
// *16 (not 17) keeps the max threshold at 240, strictly below 255 - otherwise luminance 0xFF
// (pure white) would tie the top Bayer cell's threshold and the strict ">" would misclassify
// it as black.
const uint32_t threshold = BAYER_4X4[y & 3][x & 3] * 16U;
return luminance > threshold ? 0xF : 0x0;
}
extern "C" {
extern Module m5stack_papers3_module;
@@ -34,6 +100,15 @@ struct Papers3DisplayInternal {
// Scratch buffer for the grayscale8->EPDiy(4bpp packed, 2px/byte) conversion in draw_bitmap().
uint8_t* packed_buffer;
bool powered;
uint32_t panel_pixel_count;
// Fast (MODE_DU) partial updates since the last quality refresh; see
// QUALITY_REFRESH_PARTIAL_COUNT.
uint32_t partial_count_since_quality;
// get_ticks() at the last quality refresh; see QUALITY_REFRESH_IDLE_SECONDS.
TickType_t last_quality_refresh_tick;
// While get_ticks() < this, every draw_bitmap() call uses quality mode regardless of the
// other triggers; see QUALITY_HOLD_MS.
TickType_t quality_hold_until_tick;
};
static void power_on(Papers3DisplayInternal* internal) {
@@ -64,9 +139,48 @@ static error_t papers3_display_init(Device* device) {
// pass, leaving a faint ghost. Run a full clear now, before LVGL's first flush ever reaches
// draw_bitmap(), so it never has to undo content LVGL already put on screen.
epd_fullclear(&internal->hl_state, config->temperature_celsius);
internal->partial_count_since_quality = 0;
internal->last_quality_refresh_tick = get_ticks();
internal->quality_hold_until_tick = 0;
return ERROR_NONE;
}
// Decides whether this update should be a full-quality (config->quality_draw_mode) refresh or
// a fast MODE_DU one. Read-only - see commit_quality_mode_decision() for the state this decision
// leads to.
static bool should_use_quality_mode(Papers3DisplayInternal* internal, int32_t width, int32_t height) {
const TickType_t now = get_ticks();
const uint32_t area = static_cast<uint32_t>(width) * static_cast<uint32_t>(height);
const bool is_full_screen_change = area >= static_cast<uint32_t>(
static_cast<float>(internal->panel_pixel_count) * FULL_AREA_QUALITY_THRESHOLD
);
const bool partial_count_exceeded = internal->partial_count_since_quality >= QUALITY_REFRESH_PARTIAL_COUNT;
// Idle refresh is too problematic to worth the possible gains. So it isn't done on purpose.
// Ghosting is very minimal now anyway (it's still around but way less bad)
const bool idle_exceeded = now - internal->last_quality_refresh_tick >= seconds_to_ticks(QUALITY_REFRESH_IDLE_SECONDS);
const bool within_hold = now < internal->quality_hold_until_tick;
return is_full_screen_change || partial_count_exceeded || idle_exceeded || within_hold;
}
// Applies should_use_quality_mode()'s decision, but only commits the quality-mode reset once the
// draw actually succeeded - a failed quality refresh must not make a still-ghosting panel look
// freshly cleaned to every trigger above. A failed fast update still counts toward the partial
// count, since it was still MODE_DU content, not a clean slate.
static void commit_quality_mode_decision(Papers3DisplayInternal* internal, bool used_quality, bool draw_succeeded) {
if (used_quality) {
if (draw_succeeded) {
const TickType_t now = get_ticks();
internal->partial_count_since_quality = 0;
internal->last_quality_refresh_tick = now;
internal->quality_hold_until_tick = now + millis_to_ticks(QUALITY_HOLD_MS);
}
} else {
internal->partial_count_since_quality++;
}
}
// Reports GRAYSCALE8 (not MONOCHROME) so LVGL uses partial/tile updates instead of forcing
// full-frame - the bridge hardcodes full-frame for MONOCHROME/I1 regardless of capability flags.
// So draw_bitmap is called once per changed tile, not necessarily the whole panel.
@@ -76,11 +190,12 @@ static error_t papers3_display_draw_bitmap(Device* device, int32_t x_start, int3
const int32_t width = x_end - x_start;
const int32_t height = y_end - y_start;
const bool use_quality = should_use_quality_mode(internal, width, height);
// color_data is DISPLAY_COLOR_FORMAT_GRAYSCALE8: row-major, 1 byte/pixel luminance
// (0x00=black..0xFF=white, matching LVGL's L8). EPDiy wants 4bpp packed (2px/byte, 0x0=black,
// 0xF=white) - a plain >>4 truncation preserves all 16 real gray levels the panel supports
// (this panel is not B/W-only; see MODE_GC16/GL16 in papers3-display.yaml's draw-mode doc).
// 0xF=white); Bayer dithering (full 16-level for the quality pass, binary for MODE_DU)
// spreads the rounding error instead of a flat truncation.
const auto* src = static_cast<const uint8_t*>(color_data);
const size_t src_stride = static_cast<size_t>(width);
const size_t packed_stride = static_cast<size_t>(width + 1) / 2;
@@ -90,12 +205,18 @@ static error_t papers3_display_draw_bitmap(Device* device, int32_t x_start, int3
uint8_t* dst_row = internal->packed_buffer + static_cast<size_t>(row) * packed_stride;
int32_t col = 0;
for (; col + 2 <= width; col += 2) {
const uint8_t p0 = src_row[col] >> 4U;
const uint8_t p1 = src_row[col + 1] >> 4U;
const uint8_t p0 = use_quality
? dither_to_nibble(src_row[col], x_start + col, y_start + row)
: dither_to_bw_nibble(src_row[col], x_start + col, y_start + row);
const uint8_t p1 = use_quality
? dither_to_nibble(src_row[col + 1], x_start + col + 1, y_start + row)
: dither_to_bw_nibble(src_row[col + 1], x_start + col + 1, y_start + row);
dst_row[col / 2] = static_cast<uint8_t>((p1 << 4U) | p0);
}
if (col < width) { // odd width: last column has no pair, low nibble unused
dst_row[col / 2] = static_cast<uint8_t>(src_row[col] >> 4U);
dst_row[col / 2] = use_quality
? dither_to_nibble(src_row[col], x_start + col, y_start + row)
: dither_to_bw_nibble(src_row[col], x_start + col, y_start + row);
}
}
@@ -108,13 +229,15 @@ static error_t papers3_display_draw_bitmap(Device* device, int32_t x_start, int3
power_on(internal);
epd_draw_rotated_image(update_area, internal->packed_buffer, internal->framebuffer);
const auto draw_mode = use_quality ? config->quality_draw_mode : FAST_DRAW_MODE;
auto draw_result = epd_hl_update_area(
&internal->hl_state,
static_cast<EpdDrawMode>(config->draw_mode | MODE_PACKING_2PPB),
static_cast<EpdDrawMode>(draw_mode | MODE_PACKING_2PPB),
config->temperature_celsius,
update_area
);
commit_quality_mode_decision(internal, use_quality, draw_result == EPD_DRAW_SUCCESS);
return draw_result == EPD_DRAW_SUCCESS ? ERROR_NONE : ERROR_RESOURCE;
}
@@ -133,13 +256,11 @@ static DisplayColorFormat papers3_display_get_color_format(Device*) {
return DISPLAY_COLOR_FORMAT_GRAYSCALE8;
}
// epd_width()/epd_height() are the panel's native, unrotated dimensions (display->width/height in
// epdiy.c) - epd_rotated_display_width()/height() swap them for EPD_ROT_PORTRAIT/INVERTED_PORTRAIT.
// epd_draw_rotated_image() clamps its input rect against the *rotated* dims and epd_draw_pixel()
// applies the rotation transform on top of that (see _rotate() in epdiy.c), so both LVGL's canvas
// size and draw_bitmap()'s rect must be in rotated-space, not native-space - using the native
// epd_width()/epd_height() here fed rotated-space code a landscape-sized canvas, which produced
// exactly the "rotated + landscape" symptom this was fixed for.
// epd_width()/epd_height() are the panel's native, unrotated dimensions; epd_rotated_display_
// width()/height() swap them for EPD_ROT_PORTRAIT/INVERTED_PORTRAIT. epd_draw_rotated_image()
// clamps its input rect against the rotated dims and epd_draw_pixel() applies the rotation
// transform on top of that (see _rotate() in epdiy.c), so both LVGL's canvas size and
// draw_bitmap()'s rect must be in rotated-space, not native-space.
static uint16_t papers3_display_get_resolution_x(Device*) {
return static_cast<uint16_t>(epd_rotated_display_width());
}
@@ -232,6 +353,11 @@ static error_t start(Device* device) {
return ERROR_OUT_OF_MEMORY;
}
internal->panel_pixel_count = static_cast<uint32_t>(epd_rotated_display_width()) * static_cast<uint32_t>(epd_rotated_display_height());
internal->partial_count_since_quality = 0;
internal->last_quality_refresh_tick = get_ticks();
internal->quality_hold_until_tick = 0;
device_set_driver_data(device, internal);
LOG_I(TAG, "EPDiy initialized (%dx%d native, %dx%d rotated)", epd_width(), epd_height(), epd_rotated_display_width(), epd_rotated_display_height());
@@ -10,7 +10,7 @@ extern "C" {
struct Papers3DisplayConfig {
int temperature_celsius;
enum EpdDrawMode draw_mode;
enum EpdDrawMode quality_draw_mode;
enum EpdRotation rotation;
};
@@ -29,8 +29,8 @@ static constexpr uint8_t I2C_ADDRESS = 0x6D;
static constexpr uint32_t REPEAT_INITIAL_MS = 400;
static constexpr uint32_t REPEAT_RATE_MS = 80;
// I2C event-poll interval - mirrors the old deprecated-HAL's 20ms Timer period. Drives both
// REG_INT_STAT polling (when no IRQ pin) and software key-repeat ticking.
// I2C event-poll interval
// Drives both REG_INT_STAT polling (when no IRQ pin) and software key-repeat ticking.
static constexpr uint32_t POLL_INTERVAL_MS = 20;
// Upper bound on events consumed per drain_events() call. Since the loop re-reads REG_EVENT_NUM
@@ -193,6 +193,7 @@ static uint32_t now_ms() {
// the event - and software key-repeat replays this same struct, so a held chord keeps its modifiers.
struct Tab5KeyEvent {
uint32_t key;
bool pressed;
bool ctrl;
bool alt;
uint8_t hid_keycode;
@@ -216,11 +217,20 @@ struct Tab5KeyboardInternal {
gpio_num_t irq_pin;
// Poll throttling (real-time based, since read_key() is called at whatever rate LVGL's indev
// timer and its own drain-loop - via continue_reading - happen to run at, unlike the old
// deprecated-HAL's fixed 20ms Timer)
// timer and its own drain-loop, via continue_reading, happen to run at).
uint32_t last_poll_ms;
// Software key-repeat state (tracked by position to survive modifier changes)
// Original press event for every currently-held key, indexed by matrix position (row*14+col),
// so a release can recover the exact event its press queued (modifiers captured at press
// time) even when another key was pressed and released in between. held_event[i].key can
// legitimately be 0 (e.g. F1-F12, see drain_events()), so held[i] tracks validity separately
// rather than using a sentinel key value.
Tab5KeyEvent held_event[70];
bool held[70];
// Software key-repeat state: tracks only the most recently pressed key, independent of the
// per-position storage above (repeats stop as soon as a different key is pressed, matching
// typical keyboard behavior, and don't need to survive that key's release).
Tab5KeyEvent repeat_event;
uint8_t repeat_row;
uint8_t repeat_col;
@@ -258,15 +268,17 @@ bool tab5_keyboard_is_attached(Device* device) {
}
// ---------------------------------------------------------------------------
// LED helpers - LED0 = Sym indicator (green), LED1 = Aa indicator (red)
// LED helpers - LED0 = Sym indicator (blue), LED1 = Aa indicator (red)
// RGB register layout: [B, G, R] per LED, stride 4 (byte 3 reserved)
// ---------------------------------------------------------------------------
static void update_leds(Device* device, const Tab5KeyboardInternal* internal) {
static constexpr uint8_t LED_SYM_ON_BLUE = 0xC0;
static constexpr uint8_t LED_AA_ON_RED = 0x90;
static void update_leds(Device* device, Tab5KeyboardInternal* internal) {
auto* parent = device_get_parent(device);
// [LED0: B,G,R, reserved, LED1: B,G,R]
uint8_t buf[7] = {
0x00, internal->sym_active ? uint8_t(0xA0) : uint8_t(0x00), 0x00, 0x00,
0x00, 0x00, internal->aa_sticky ? uint8_t(0xA0) : uint8_t(0x00),
internal->sym_active ? LED_SYM_ON_BLUE : uint8_t(0x00), 0x00, 0x00, 0x00,
0x00, 0x00, internal->aa_sticky ? LED_AA_ON_RED : uint8_t(0x00),
};
i2c_controller_write_register(parent, I2C_ADDRESS, REG_RGB_BASE, buf, 7, pdMS_TO_TICKS(50));
}
@@ -389,9 +401,14 @@ static void drain_events(Device* device, Tab5KeyboardInternal* internal) {
// no business reaching into, so ESC is now just queued as a normal key
// like everything else (LVGL/app code already handles ESC via focus/group
// navigation the same way a dedicated ESC key on any other keyboard would).
const Tab5KeyEvent event = { lv_key, internal->ctrl_held, internal->alt_held,
const Tab5KeyEvent event = { lv_key, true, internal->ctrl_held, internal->alt_held,
m.keycode, modifier };
xQueueSend(internal->queue, &event, 0);
// Remember this key's press event by position so its release (whenever it
// comes, regardless of what else is pressed in between) reports the same value.
const uint8_t idx = row * 14U + col;
internal->held_event[idx] = event;
internal->held[idx] = true;
// Arm software repeat tracking by row/col to survive modifier changes
const uint32_t now = now_ms();
internal->repeat_event = event;
@@ -405,9 +422,31 @@ static void drain_events(Device* device, Tab5KeyboardInternal* internal) {
internal->aa_held = false;
update_leds(device, internal);
}
} else if (row == internal->repeat_row && col == internal->repeat_col) {
// Match release by position, not translated value — survives sticky Aa clear
internal->repeat_event.key = 0;
} else {
// Always queue the release: callers key their own "is this held" state off
// (key, pressed) pairs, and a dropped release leaves that key stuck down forever.
//
// Reuse the matching press's key/modifier (via held_event, tracked by matrix
// position) rather than recomputing from current modifier state: aa_active
// above is read fresh, but sticky Aa is consumed right after the press fires
// (above), so recomputing here would give the release a different key value
// than its press whenever Aa was sticky (e.g. shifted vs unshifted). Only
// recompute as a fallback for the case where no press was ever recorded for
// this position (e.g. driver just started while the key was already held).
const uint8_t idx = row * 14U + col;
Tab5KeyEvent event;
if (internal->held[idx]) {
event = internal->held_event[idx];
internal->held[idx] = false;
} else {
event = { lv_key, false, internal->ctrl_held, internal->alt_held, m.keycode, modifier };
}
event.pressed = false;
xQueueSend(internal->queue, &event, 0);
if (row == internal->repeat_row && col == internal->repeat_col) {
internal->repeat_event.key = 0;
}
}
}
}
@@ -429,7 +468,7 @@ void tab5_keyboard_reinit(Device* device) {
write_reg_fast(device, REG_EVENT_NUM, 0x00); // flush event queue
write_reg_fast(device, REG_INT_STAT, 0x00); // clear pending INT
write_reg_fast(device, REG_RGB_MODE, 0x01); // Custom RGB mode (manual LED control)
write_reg_fast(device, REG_BRIGHTNESS, 50); // 50% brightness
write_reg_fast(device, REG_BRIGHTNESS, 30); // 30% brightness
update_leds(device, internal); // restore current LED state
if (internal->irq_configured) {
@@ -437,9 +476,39 @@ void tab5_keyboard_reinit(Device* device) {
}
}
void tab5_keyboard_reset_state(Device* device) {
auto* internal = static_cast<Tab5KeyboardInternal*>(device_get_driver_data(device));
for (uint8_t idx = 0; idx < 70U; idx++) {
if (internal->held[idx]) {
Tab5KeyEvent event = internal->held_event[idx];
event.pressed = false;
// This runs under lvgl_lock() (apply_state()'s caller), and the consumer that drains
// this queue is LVGL's own indev read callback - blocking here for queue space could
// deadlock against that consumer needing the same lock. Best-effort send only, same as
// drain_events()'s hot path; internal->held[idx] is still cleared on a dropped send so
// this reset doesn't get stuck retrying a release that already lost its only chance to
// be delivered before the state it referred to (an unplugged keyboard) is gone anyway.
xQueueSend(internal->queue, &event, 0);
internal->held[idx] = false;
}
}
internal->repeat_event.key = 0;
internal->repeat_row = 0xFF;
internal->repeat_col = 0xFF;
internal->sym_active = false;
internal->aa_sticky = false;
internal->aa_held = false;
internal->aa_tapped = false;
internal->ctrl_held = false;
internal->alt_held = false;
update_leds(device, internal);
}
// ---------------------------------------------------------------------------
// poll_if_due - the closest equivalent to the old deprecated-HAL's 20ms-Timer-driven
// processKeyboard(): drains new key events (IRQ-gated or polled) and ticks software key-repeat.
// poll_if_due - drains new key events (IRQ-gated or polled) and ticks software key-repeat.
// Called from read_key(), throttled to real elapsed time rather than call count, since read_key()
// can be called back-to-back multiple times per LVGL indev timer tick while draining an
// already-queued burst (continue_reading). Hot-plug attach detection lives outside the driver -
@@ -566,7 +635,7 @@ static error_t tab5_keyboard_read_key(Device* device, KeyboardKeyData* data) {
Tab5KeyEvent event = {};
if (xQueueReceive(internal->queue, &event, 0) == pdTRUE) {
data->key = event.key;
data->pressed = true;
data->pressed = event.pressed;
data->continue_reading = uxQueueMessagesWaiting(internal->queue) > 0;
data->ctrl = event.ctrl;
data->alt = event.alt;
@@ -37,6 +37,12 @@ bool tab5_keyboard_is_attached(struct Device* device);
// tab5_keyboard_attach_detect.cpp).
void tab5_keyboard_reinit(struct Device* device);
// Emits a release for every currently-held key (a hardware release can't arrive once the keyboard
// is unplugged), then clears held-key/software-repeat/modifier state. Callers must call this on
// confirmed detach (see tab5_keyboard_attach_detect.cpp) so a key held across an unplug doesn't
// leave consumers with a stuck key or spurious repeats/modifiers after reattach.
void tab5_keyboard_reset_state(struct Device* device);
#ifdef __cplusplus
}
#endif
@@ -63,6 +63,11 @@ static bool apply_state(Device* keyboard_device, bool attached) {
lv_display_set_rotation(display, LV_DISPLAY_ROTATION_90);
}
} else {
// A key held at the moment of unplug can never get its hardware release - reset tracked
// state so it doesn't read as stuck or feed stale modifiers into whatever's pressed next
// after reattach.
tab5_keyboard_reset_state(keyboard_device);
// Only restore if rotation is still what we set it to - if the user manually changed it
// since attaching, respect their choice instead.
if (rotation_override_active && lv_display_get_rotation(display) == LV_DISPLAY_ROTATION_90) {
@@ -84,13 +89,24 @@ static void attach_detect_callback(TimerHandle_t /*timer*/) {
// LVGL restarting is a distinct event from the keyboard physically attaching/detaching: the
// accessory may never have moved, but whatever apply_state() last set (rotation) may have
// been reset in the meantime by the restart. Forcing was_attached false makes the block below
// see a fresh "attached" transition (still going through the normal 2-check debounce) so
// see a fresh "attached" transition, still going through the normal two-check debounce, so
// apply_state() re-announces the current state instead of staying silent forever, waiting for
// an edge that will never come because the keyboard was never actually unplugged.
// lvgl_is_running() is safe to call unlocked (unlike lv_display_get_default(), resolved inside
// the lock in apply_state() instead).
const bool lvgl_ready = lvgl_is_running();
if (lvgl_ready && !was_lvgl_ready) {
// If the keyboard is (and, per was_attached, already was) physically detached, forcing
// was_attached false below means the detach transition below will never fire again for
// this unplug - it already happened before this restart. Reset software state here
// instead, since apply_state()'s own detach path may never have run: it could have bailed
// out early (LVGL lock busy, or display not ready yet) before reaching its
// tab5_keyboard_reset_state() call, and now never will, because that transition is about
// to be erased. This reset doesn't touch LVGL/display state, so it doesn't need
// apply_state()'s lock/display gating.
if (was_attached && !tab5_keyboard_is_attached(keyboard_device)) {
tab5_keyboard_reset_state(keyboard_device);
}
was_attached = false;
pending_attach_confirm_count = 0;
}
@@ -98,7 +114,7 @@ static void attach_detect_callback(TimerHandle_t /*timer*/) {
const bool attached = tab5_keyboard_is_attached(keyboard_device);
if (attached != was_attached) {
// Require the new state to be confirmed on a second consecutive check before acting - a
// Require the new state to be confirmed on a second consecutive check before acting: a
// single probe on a floating/half-connected bus (e.g. mid-unplug) can false-positive.
if (attached != pending_attach_state || pending_attach_confirm_count == 0) {
pending_attach_state = attached;
@@ -108,7 +124,7 @@ static void attach_detect_callback(TimerHandle_t /*timer*/) {
if (apply_state(keyboard_device, attached)) {
was_attached = attached;
}
// else: not handled yet (e.g. LVGL lock busy) - retry on the next confirmed check
// else: not handled yet (e.g. LVGL lock busy); retry on the next confirmed check
}
} else {
pending_attach_confirm_count = 0;
+1 -1
View File
@@ -174,7 +174,7 @@
i2c_keyboard: i2c2 {
compatible = "espressif,esp32-i2c-master";
port = <LP_I2C_NUM_0>;
clock-frequency = <100000>;
clock-frequency = <400000>;
clock-source = <LP_I2C_SCLK_DEFAULT>;
pin-sda = <&gpio0 0 GPIO_FLAG_PULL_UP>;
pin-scl = <&gpio0 1 GPIO_FLAG_PULL_UP>;
+2 -1
View File
@@ -13,7 +13,8 @@ if (NOT DEFINED ENV{ESP_IDF_VERSION})
)
target_link_libraries(simulator
PRIVATE Tactility
PRIVATE TactilityKernel
PRIVATE TactilityFreeRtos
PRIVATE lvgl
PRIVATE SDL2-static
)
+105 -38
View File
@@ -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));
SDL_DestroyTexture(internal->texture);
SDL_DestroyRenderer(internal->renderer);
SDL_DestroyWindow(internal->window);
SDL_QuitSubSystem(SDL_INIT_VIDEO);
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
+34 -4
View File
@@ -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:
+1 -1
View File
@@ -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 {};
+3 -17
View File
@@ -7,31 +7,22 @@
## Higher Priority
- Add tests for app stdin/stdout
- CrashDiagnostics shouldn't show a QR when there's no callstack
- Apps currently have a `Context` object with an `appInstanceId` in it, purely for being able to close the app.
Change it so that the app has its own termination signal that it waits for in the loop, it should subscribe to the event group.
- stopAppFromToolbar() in Tactility.cpp stops the top-most app. Change it so the toolbar knows for which app id it is created, so it can rely on that.
- Warn if file operations are done from prohibited tasks (e.g. lvgl task)
- Move USB host task stacks to SPIRAM when available: esp32_usbhost*.cpp
- Get rid of WiFi service (Wifi.cpp/h) in Tactility.cpp
- Make it more clear to end-users that an SD card is required to run Tactility
- Make it possible to override stack size for an app via config file (loaded at boot), and make it possible to set preferred memory location (e.g. internal/external)
- Add bold fonts for e-ink readability improvement
- Httpd.cpp: warn if running on same CPU core (or task) as UI/LVGL/window manager.
- Improve Setup: Show "Step done" screen
- Improve Setup: Add keyboard/keypad navigation explanation
- display.h API: get_backlight does not change ref counting, but it should
- bluetooth: various getters for child devices do not change ref counting, but they should (e.g. bluetooth_hid_device_get_device())
- Improve kernel_init.cpp (and other modules): create driver_ensure_added() and driver_ensure_destructed()
- Drivers/audio-codec-module is not a module. Move it somewhere else. Or make it an actual module.
- LilyGO T-Dongle S3: 1 button control, stop auto-launching web server
- Core2: support power off via software
- Create `#define` for empty module (for modules that fully rely on device.properties and don't define drivers or have start/stop logic)
- Get rid of TactilityC in favour of TactilityKernel and kernel modules
- Improve SPI kernel driver (implement read, write, transactions)
- Add font design tokens such as "regular", "title" and "smaller". Perhaps via the LVGL kernel module.
- Fix glitches when installing app via App Hub with 4.3" Waveshare
- TCA9534 keyboards should use interrupts
- External app loading: Check the version of Tactility and check ESP target hardware to check for compatibility
Check during installation process, but also when starting (SD card might have old app install from before Tactility OS update)
@@ -45,6 +36,7 @@
## Medium Priority
- lvgl-module's spinner relies on hard-coded spinner asset from Tactility main project.
- esp_lvgl_port settings has a large stack size (~9kB) to fix stackoverflow when LVGL events (e.g. button click) do actions like file operations do actions like file operations. Can we reduce the callstack?
- `struct Driver` has an `.owner`, but it's not always set. Either validate on Module construct that it matches, or otherwise set it during module start. The problem: NULL parent currently means that driver is not removable. This clashes with setting it dynamically. Consider some kind of flag to determine removability.
- Consider moving certain drivers into separate modules: audio, bt, wifi, etc
@@ -52,9 +44,9 @@
- Consider implementing LVGL gridnav in apps https://lvgl.io/docs/open/9.3/details/auxiliary-modules/gridnav.html
- Make USB host driver disabled by default, so it doesn't consume memory
- TactilityTool: Make API compatibility table (and check for compatibility in the tool itself)
- Improve EspLcdDisplay to contain all the standard configuration options, and implement a default init function. Add a configuration class.
- Unify the way displays are dimmed. Some implementations turn off the display when it's fully dimmed. Make this a separate functionality.
- Bug: Crash handling app cannot be exited with an EncoderDevice. (current work-around is to manually reset the device)
- Refactor HttpServer into C code and move implementation to http-module
- Use GPS time to set/update the current time
## Lower Priority
@@ -62,20 +54,14 @@
- lvgl-module has a keyboard.cpp that creates a `keyboard_group`. This group is set as the default group, so it can also work with trackball(= LVGL "encoder").
Make a separate group that is the default group. The keyboard can then use it (or use its own).
The basic idea is to invert the ownership: now the keyboard group is made the default group, but it's probably more logical to have the default group used by the keyboard.
- lvgl-module's spinner relies on hard-coded spinner asset from Tactility main project.
- Localize all apps
- Support hot-plugging SD card (note: this is not possible if they require the CS pin hack)
- Explore LVGL9's FreeRTOS functionality
- CrashHandler: use "corrupted" flag
- CrashHandler: process other types of crashes (WDT?)
- Use GPS time to set/update the current time
- Consider using non_null (either via MS GSL, or custom)
- Fix system time to not be 1980 (use build year as a minimum). Consider keeping track of the last known time.
- Use std::span or string_view in StringUtils https://youtu.be/FRkJCvHWdwQ?t=2754
- Mutex: Implement give/take from ISR support (works only for non-recursive ones)
- Show a warning screen if firmware encryption or secure boot are off when saving WiFi credentials.
- Remove flex_flow from app_container in Gui.cpp
- Bug: CYD 2432S032C screen rotation fails due to touch driver issue
- Calculator app should show regular text input field on non-touch devices that have a keyboard (Cardputer, T-Lora Pager)
- Allow for WSAD keys to navigate LVGL (this is extra nice for cardputer, but just handy in general)
- Create a "How to" app for a device. It could explain things like keyboard navigation on first start.
@@ -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");
+11
View File
@@ -0,0 +1,11 @@
cmake_minimum_required(VERSION 3.20)
include("${CMAKE_CURRENT_LIST_DIR}/../../Buildscripts/module.cmake")
file(GLOB_RECURSE SOURCE_FILES "source/*.c*")
tactility_add_module(st77922-module
SRCS ${SOURCE_FILES}
INCLUDE_DIRS include/
REQUIRES TactilityKernel platform-esp32 esp_lcd_st77922 driver
)
@@ -0,0 +1,37 @@
description: Touch interface integrated in the Sitronix ST77922 TDDI
include: ["i2c-device.yaml"]
compatible: "sitronix,st77922-touch"
bus: i2c
properties:
x-max:
type: int
required: true
description: Maximum X coordinate
y-max:
type: int
required: true
description: Maximum Y coordinate
swap-xy:
type: boolean
default: false
description: Swap the X and Y axes
mirror-x:
type: boolean
default: false
description: Mirror the X axis
mirror-y:
type: boolean
default: false
description: Mirror the Y axis
pin-reset:
type: phandles
default: GPIO_PIN_SPEC_NONE
description: Reset GPIO pin
pin-interrupt:
type: phandles
default: GPIO_PIN_SPEC_NONE
description: Interrupt GPIO pin
@@ -0,0 +1,47 @@
description: Sitronix ST77922 QSPI display panel
compatible: "sitronix,st77922"
bus: spi
properties:
horizontal-resolution:
type: int
required: true
description: Horizontal resolution in pixels
vertical-resolution:
type: int
required: true
description: Vertical resolution in pixels
mirror-x:
type: boolean
default: false
description: Mirror the X axis
mirror-y:
type: boolean
default: false
description: Mirror the Y axis
invert-color:
type: boolean
default: false
description: Invert the panel's color output
bgr-order:
type: boolean
default: false
description: Use BGR element order instead of RGB
bits-per-pixel:
type: int
default: 16
description: Color depth in bits per pixel
pixel-clock-hz:
type: int
default: 80000000
description: QSPI pixel clock frequency in Hz
transaction-queue-depth:
type: int
default: 10
description: Size of the internal SPI transaction queue
backlight:
type: phandle
default: "NULL"
description: Optional reference to this display's backlight device
+3
View File
@@ -0,0 +1,3 @@
dependencies:
- TactilityKernel
bindings: bindings
@@ -0,0 +1,7 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once
#include <tactility/bindings/bindings.h>
#include <drivers/st77922.h>
DEFINE_DEVICETREE(st77922, struct St77922Config)
@@ -0,0 +1,7 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once
#include <tactility/bindings/bindings.h>
#include <drivers/st77922_touch.h>
DEFINE_DEVICETREE(st77922_touch, struct St77922TouchConfig)
@@ -0,0 +1,20 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once
#include <stdbool.h>
#include <stdint.h>
#include <tactility/device.h>
struct St77922Config {
uint16_t horizontal_resolution;
uint16_t vertical_resolution;
bool mirror_x;
bool mirror_y;
bool invert_color;
bool bgr_order;
uint32_t bits_per_pixel;
uint32_t pixel_clock_hz;
uint8_t transaction_queue_depth;
struct Device* backlight;
};
@@ -0,0 +1,18 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once
#include <stdbool.h>
#include <stdint.h>
#include <tactility/drivers/gpio.h>
struct St77922TouchConfig {
uint8_t address;
uint16_t x_max;
uint16_t y_max;
bool swap_xy;
bool mirror_x;
bool mirror_y;
struct GpioPinSpec pin_reset;
struct GpioPinSpec pin_interrupt;
};
@@ -0,0 +1,6 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once
#include <tactility/module.h>
extern Module st77922_module;
+21
View File
@@ -0,0 +1,21 @@
// SPDX-License-Identifier: Apache-2.0
#include <tactility/driver.h>
#include <tactility/module.h>
extern "C" {
extern Driver st77922_driver;
extern Driver st77922_touch_driver;
static Driver* const st77922_drivers[] = {
&st77922_driver,
&st77922_touch_driver,
nullptr
};
Module st77922_module = {
.name = "st77922",
.drivers = st77922_drivers
};
} // extern "C"
+270
View File
@@ -0,0 +1,270 @@
// SPDX-License-Identifier: Apache-2.0
#include <drivers/st77922.h>
#include <st77922_module.h>
#include "st77922_init.h"
#include <tactility/check.h>
#include <tactility/device.h>
#include <tactility/driver.h>
#include <tactility/drivers/display.h>
#include <tactility/drivers/esp32_spi.h>
#include <tactility/drivers/spi_controller.h>
#include <tactility/log.h>
#include <esp_err.h>
#include <esp_heap_caps.h>
#include <esp_lcd_io_spi.h>
#include <esp_lcd_panel_io.h>
#include <esp_lcd_panel_ops.h>
#include <esp_lcd_st77922.h>
#include <freertos/semphr.h>
#include <cstdlib>
#define TAG "ST77922"
#define GET_CONFIG(device) (static_cast<const St77922Config*>((device)->config))
struct St77922Internal {
esp_lcd_panel_io_handle_t io_handle;
esp_lcd_panel_handle_t panel_handle;
SemaphoreHandle_t draw_done;
uint8_t* transfer_buffer;
size_t transfer_buffer_size;
};
static bool IRAM_ATTR transfer_done(esp_lcd_panel_io_handle_t, esp_lcd_panel_io_event_data_t*, void* context) {
auto* internal = static_cast<St77922Internal*>(context);
BaseType_t task_woken = pdFALSE;
xSemaphoreGiveFromISR(internal->draw_done, &task_woken);
return task_woken == pdTRUE;
}
static error_t start(Device* device) {
auto* parent = device_get_parent(device);
check(device_get_type(parent) == &SPI_CONTROLLER_TYPE);
const auto* spi = static_cast<const Esp32SpiConfig*>(parent->config);
const auto* config = GET_CONFIG(device);
GpioPinSpec cs;
if (esp32_spi_get_cs_pin(device, &cs) != ERROR_NONE) {
return ERROR_RESOURCE;
}
auto* internal = static_cast<St77922Internal*>(calloc(1, sizeof(St77922Internal)));
if (internal == nullptr) {
return ERROR_OUT_OF_MEMORY;
}
internal->draw_done = xSemaphoreCreateBinary();
if (internal->draw_done == nullptr) {
free(internal);
return ERROR_OUT_OF_MEMORY;
}
// The vendor port renders a complete frame in PSRAM, then copies it through a
// 1/10-frame DMA buffer. Besides making full-frame refresh possible, this keeps
// the panel's GRAM synchronized when animated objects invalidate old and new
// positions in separate LVGL regions.
const size_t bytes_per_pixel = config->bits_per_pixel / 8;
const size_t rows_per_transfer = config->vertical_resolution > 10
? config->vertical_resolution / 10 : config->vertical_resolution;
internal->transfer_buffer_size =
static_cast<size_t>(config->horizontal_resolution) * rows_per_transfer * bytes_per_pixel;
if (spi->max_transfer_size > 0
&& internal->transfer_buffer_size > static_cast<size_t>(spi->max_transfer_size)) {
internal->transfer_buffer_size = static_cast<size_t>(spi->max_transfer_size);
}
internal->transfer_buffer = static_cast<uint8_t*>(heap_caps_malloc(
internal->transfer_buffer_size, MALLOC_CAP_DMA | MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT));
if (internal->transfer_buffer == nullptr) {
vSemaphoreDelete(internal->draw_done);
free(internal);
return ERROR_OUT_OF_MEMORY;
}
esp_lcd_panel_io_spi_config_t io_config = {
.cs_gpio_num = static_cast<int>(cs.pin),
.dc_gpio_num = -1,
.spi_mode = 0,
.pclk_hz = config->pixel_clock_hz,
.trans_queue_depth = config->transaction_queue_depth,
.on_color_trans_done = transfer_done,
.user_ctx = internal,
.lcd_cmd_bits = 32,
.lcd_param_bits = 8,
.cs_ena_pretrans = 0,
.cs_ena_posttrans = 0,
.flags = {
.dc_high_on_cmd = 0,
.dc_low_on_data = 0,
.dc_low_on_param = 0,
.octal_mode = 0,
.quad_mode = 1,
.sio_mode = 0,
.lsb_first = 0,
.cs_high_active = 0,
},
};
esp_err_t result = esp_lcd_new_panel_io_spi(
static_cast<esp_lcd_spi_bus_handle_t>(spi->host), &io_config, &internal->io_handle);
if (result != ESP_OK) {
LOG_E(TAG, "Failed to create panel IO: %s", esp_err_to_name(result));
heap_caps_free(internal->transfer_buffer);
vSemaphoreDelete(internal->draw_done);
free(internal);
return ERROR_RESOURCE;
}
size_t init_count = 0;
st77922_vendor_config_t vendor = {
.init_cmds = st77922_board_init_commands(&init_count),
.init_cmds_size = static_cast<uint16_t>(init_count),
.flags = { .use_qspi_interface = 1 },
};
esp_lcd_panel_dev_config_t panel_config = {
.reset_gpio_num = -1,
.rgb_ele_order = config->bgr_order ? LCD_RGB_ELEMENT_ORDER_BGR : LCD_RGB_ELEMENT_ORDER_RGB,
.data_endian = LCD_RGB_DATA_ENDIAN_LITTLE,
.bits_per_pixel = config->bits_per_pixel,
.flags = { .reset_active_high = false },
.vendor_config = &vendor,
};
result = esp_lcd_new_panel_st77922(internal->io_handle, &panel_config, &internal->panel_handle);
bool ok = result == ESP_OK;
ok = ok && esp_lcd_panel_reset(internal->panel_handle) == ESP_OK;
ok = ok && esp_lcd_panel_init(internal->panel_handle) == ESP_OK;
ok = ok && ((!config->mirror_x && !config->mirror_y)
|| esp_lcd_panel_mirror(internal->panel_handle, config->mirror_x, config->mirror_y) == ESP_OK);
ok = ok && (!config->invert_color || esp_lcd_panel_invert_color(internal->panel_handle, true) == ESP_OK);
ok = ok && esp_lcd_panel_disp_on_off(internal->panel_handle, true) == ESP_OK;
if (!ok) {
LOG_E(TAG, "Failed to bring up panel: %s", esp_err_to_name(result));
if (internal->panel_handle != nullptr) esp_lcd_panel_del(internal->panel_handle);
esp_lcd_panel_io_del(internal->io_handle);
heap_caps_free(internal->transfer_buffer);
vSemaphoreDelete(internal->draw_done);
free(internal);
return ERROR_RESOURCE;
}
device_set_driver_data(device, internal);
return ERROR_NONE;
}
static error_t stop(Device* device) {
auto* internal = static_cast<St77922Internal*>(device_get_driver_data(device));
if (esp_lcd_panel_del(internal->panel_handle) != ESP_OK
|| esp_lcd_panel_io_del(internal->io_handle) != ESP_OK) {
return ERROR_RESOURCE;
}
heap_caps_free(internal->transfer_buffer);
vSemaphoreDelete(internal->draw_done);
free(internal);
device_set_driver_data(device, nullptr);
return ERROR_NONE;
}
static error_t reset(Device* device) {
auto* data = static_cast<St77922Internal*>(device_get_driver_data(device));
return esp_lcd_panel_reset(data->panel_handle) == ESP_OK ? ERROR_NONE : ERROR_RESOURCE;
}
static error_t init(Device* device) {
auto* data = static_cast<St77922Internal*>(device_get_driver_data(device));
return esp_lcd_panel_init(data->panel_handle) == ESP_OK ? ERROR_NONE : ERROR_RESOURCE;
}
static error_t draw_bitmap(Device* device, int32_t xs, int32_t ys, int32_t xe, int32_t ye, const void* pixels) {
auto* data = static_cast<St77922Internal*>(device_get_driver_data(device));
const auto* config = GET_CONFIG(device);
const size_t row_bytes = static_cast<size_t>(xe - xs) * config->bits_per_pixel / 8;
if (row_bytes == 0 || row_bytes > data->transfer_buffer_size) {
return ERROR_OUT_OF_RANGE;
}
const size_t rows_per_transfer = data->transfer_buffer_size / row_bytes;
const auto* source = static_cast<const uint8_t*>(pixels);
int32_t chunk_y = ys;
while (chunk_y < ye) {
const size_t remaining_rows = static_cast<size_t>(ye - chunk_y);
const size_t chunk_rows = remaining_rows < rows_per_transfer ? remaining_rows : rows_per_transfer;
const size_t chunk_bytes = chunk_rows * row_bytes;
memcpy(data->transfer_buffer, source, chunk_bytes);
xSemaphoreTake(data->draw_done, 0);
if (esp_lcd_panel_draw_bitmap(
data->panel_handle, xs, chunk_y, xe, chunk_y + static_cast<int32_t>(chunk_rows),
data->transfer_buffer) != ESP_OK) {
LOG_E(TAG, "Failed to queue color transfer at y=%ld", static_cast<long>(chunk_y));
return ERROR_RESOURCE;
}
if (xSemaphoreTake(data->draw_done, pdMS_TO_TICKS(1000)) != pdTRUE) {
LOG_E(TAG, "Timed out waiting for color transfer at y=%ld", static_cast<long>(chunk_y));
return ERROR_TIMEOUT;
}
source += chunk_bytes;
chunk_y += static_cast<int32_t>(chunk_rows);
}
return ERROR_NONE;
}
static error_t mirror(Device* device, bool x, bool y) {
auto* data = static_cast<St77922Internal*>(device_get_driver_data(device));
return esp_lcd_panel_mirror(data->panel_handle, x, y) == ESP_OK ? ERROR_NONE : ERROR_RESOURCE;
}
static bool get_mirror_x(Device* device) { return GET_CONFIG(device)->mirror_x; }
static bool get_mirror_y(Device* device) { return GET_CONFIG(device)->mirror_y; }
static error_t invert(Device* device, bool value) {
auto* data = static_cast<St77922Internal*>(device_get_driver_data(device));
return esp_lcd_panel_invert_color(data->panel_handle, value) == ESP_OK ? ERROR_NONE : ERROR_RESOURCE;
}
static error_t on_off(Device* device, bool value) {
auto* data = static_cast<St77922Internal*>(device_get_driver_data(device));
return esp_lcd_panel_disp_on_off(data->panel_handle, value) == ESP_OK ? ERROR_NONE : ERROR_RESOURCE;
}
static error_t sleep(Device* device, bool value) {
auto* data = static_cast<St77922Internal*>(device_get_driver_data(device));
return esp_lcd_panel_disp_sleep(data->panel_handle, value) == ESP_OK ? ERROR_NONE : ERROR_RESOURCE;
}
static DisplayColorFormat color_format(Device*) { return DISPLAY_COLOR_FORMAT_RGB565_SWAPPED; }
static uint16_t resolution_x(Device* device) { return GET_CONFIG(device)->horizontal_resolution; }
static uint16_t resolution_y(Device* device) { return GET_CONFIG(device)->vertical_resolution; }
static void frame_buffer(Device*, uint8_t, void** output) { *output = nullptr; }
static uint8_t frame_buffer_count(Device*) { return 0; }
static error_t backlight(Device* device, Device** output) {
*output = GET_CONFIG(device)->backlight;
return *output == nullptr ? ERROR_NOT_SUPPORTED : ERROR_NONE;
}
static const DisplayApi display_api = {
.capabilities = DISPLAY_CAPABILITY_CAP_MIRROR | DISPLAY_CAPABILITY_INVERT_COLOR |
DISPLAY_CAPABILITY_ON_OFF | DISPLAY_CAPABILITY_SLEEP | DISPLAY_CAPABILITY_BACKLIGHT |
DISPLAY_CAPABILITY_REQUIRES_FULL_FRAME,
.reset = reset,
.init = init,
.draw_bitmap = draw_bitmap,
.mirror = mirror,
.swap_xy = nullptr,
.get_swap_xy = nullptr,
.get_mirror_x = get_mirror_x,
.get_mirror_y = get_mirror_y,
.set_gap = nullptr,
.get_gap_x = nullptr,
.get_gap_y = nullptr,
.invert_color = invert,
.disp_on_off = on_off,
.disp_sleep = sleep,
.get_color_format = color_format,
.get_resolution_x = resolution_x,
.get_resolution_y = resolution_y,
.get_frame_buffer = frame_buffer,
.get_frame_buffer_count = frame_buffer_count,
.get_backlight = backlight,
.has_capability = nullptr,
};
Driver st77922_driver = {
.name = "st77922",
.compatible = (const char*[]) { "sitronix,st77922", nullptr },
.start_device = start,
.stop_device = stop,
.api = &display_api,
.device_type = &DISPLAY_TYPE,
.owner = &st77922_module,
.internal = nullptr
};
@@ -0,0 +1,74 @@
// SPDX-License-Identifier: Apache-2.0
#include "st77922_init.h"
// Initialization sequence supplied with the LCDWIKI/Hosyond ES3C35P vendor ESP-IDF demo.
static const st77922_lcd_init_cmd_t init_commands[] = {
{0xF1, (uint8_t []){0x00}, 1, 0},
{0x60, (uint8_t []){0x00, 0x00, 0x00}, 3, 0},
{0x65, (uint8_t []){0x80}, 1, 0},
{0x79, (uint8_t []){0x06}, 1, 0},
{0x7B, (uint8_t []){0x00, 0x08, 0x08}, 3, 0},
{0x80, (uint8_t []){0x55, 0x62, 0x2F, 0x17, 0xF0, 0x52, 0x70, 0xD2, 0x52, 0x62, 0xEA}, 11, 0},
{0x81, (uint8_t []){0x26, 0x52, 0x72, 0x27}, 4, 0},
{0x84, (uint8_t []){0x92, 0x25}, 2, 0},
{0x87, (uint8_t []){0x10, 0x10, 0x58, 0x00, 0x02, 0x3A}, 6, 0},
{0x88, (uint8_t []){0x00, 0x00, 0x2C, 0x10, 0x04, 0x00, 0x00, 0x00, 0x01, 0x01, 0x01, 0x01, 0x01, 0x00, 0x06}, 15, 0},
{0x89, (uint8_t []){0x00, 0x00, 0x00}, 3, 0},
{0x8A, (uint8_t []){0x13, 0x00, 0x2C, 0x00, 0x00, 0x2C, 0x10, 0x10, 0x00, 0x3E, 0x19}, 11, 0},
{0x8B, (uint8_t []){0x15, 0xB1, 0xB1, 0x44, 0x96, 0x2C, 0x10, 0x97, 0x8E}, 9, 0},
{0x8C, (uint8_t []){0x1D, 0xB1, 0xB1, 0x44, 0x96, 0x2C, 0x10, 0x50, 0x0F, 0x01, 0xC5, 0x12, 0x09}, 13, 0},
{0x8D, (uint8_t []){0x0C}, 1, 0},
{0x8E, (uint8_t []){0x33, 0x01, 0x0C, 0x13, 0x01, 0x01}, 6, 0},
{0xB3, (uint8_t []){0x00, 0x30}, 2, 0},
{0xF1, (uint8_t []){0x00}, 1, 0},
{0x71, (uint8_t []){0xD0}, 1, 0},
{0x66, (uint8_t []){0x02, 0x3F}, 2, 0},
{0xBE, (uint8_t []){0x26, 0x00, 0x9D}, 3, 0},
{0x70, (uint8_t []){0x01, 0xA0, 0x11, 0x40, 0xE0, 0x00, 0x11, 0x69, 0x11, 0x00, 0x00, 0x1A}, 12, 0},
{0x90, (uint8_t []){0x04, 0x04, 0x55, 0x74, 0x00, 0x40, 0x43, 0x27, 0x27}, 9, 0},
{0x91, (uint8_t []){0x04, 0x04, 0x55, 0x75, 0x00, 0x40, 0x42, 0x27, 0x27}, 9, 0},
{0x92, (uint8_t []){0x04, 0x44, 0x55, 0xC0, 0x06, 0x00, 0x07, 0x05, 0x90, 0x27}, 10, 0},
{0x93, (uint8_t []){0x04, 0x43, 0x11, 0x00, 0x00, 0x00, 0x00, 0x05, 0x90, 0x27}, 10, 0},
{0x94, (uint8_t []){0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, 6, 0},
{0x95, (uint8_t []){0x96, 0x16, 0x00, 0x00, 0xFF}, 5, 0},
{0x96, (uint8_t []){0x44, 0x53, 0x03, 0x12, 0x23, 0x24, 0x06, 0x05, 0x94, 0x27, 0x00, 0x44}, 12, 0},
{0x97, (uint8_t []){0x44, 0x53, 0x47, 0x56, 0x20, 0x20, 0x02, 0x01, 0x94, 0x27, 0x00, 0x44}, 12, 0},
{0xBA, (uint8_t []){0x55, 0x94, 0x2D, 0x94, 0x27}, 5, 0},
{0x9A, (uint8_t []){0x40, 0x00, 0x06, 0x00, 0x00, 0x00, 0x00}, 7, 0},
{0x9B, (uint8_t []){0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x00}, 7, 0},
{0x9C, (uint8_t []){0x5C, 0x12, 0x00, 0x00, 0x10, 0x12, 0x00, 0x00, 0x10, 0x02, 0x00, 0x00, 0x00}, 13, 0},
{0x9D, (uint8_t []){0x8A, 0x51, 0x00, 0x00, 0x00, 0x80, 0x1E, 0x01}, 8, 0},
{0x9E, (uint8_t []){0x51, 0x00, 0x00, 0x00, 0x80, 0x1E, 0x01}, 7, 0},
{0xB4, (uint8_t []){0x1D, 0x1C, 0x1E, 0x0B, 0x14, 0x02, 0x13, 0x09, 0x1E, 0x00, 0x1E, 0x10}, 12, 0},
{0xB5, (uint8_t []){0x1D, 0x1C, 0x1E, 0x0A, 0x15, 0x03, 0x11, 0x08, 0x1E, 0x01, 0x1E, 0x12}, 12, 0},
{0xB6, (uint8_t []){0x77, 0x77, 0x00, 0x0A, 0xFF, 0x0A, 0xFF}, 7, 0},
{0x86, (uint8_t []){0xCD, 0x04, 0xB1, 0x02, 0x58, 0x12, 0x58, 0x0C, 0x13, 0x01, 0xA5, 0x00, 0xA5, 0xA5}, 14, 0},
{0xB7, (uint8_t []){0x07, 0x0A, 0x0E, 0x06, 0x05, 0x03, 0x2B, 0x03, 0x03, 0x42, 0x07, 0x10, 0x10, 0x2E, 0x3F, 0x0D}, 16, 0},
{0xB8, (uint8_t []){0x07, 0x0A, 0x0D, 0x05, 0x05, 0x02, 0x2B, 0x02, 0x03, 0x42, 0x06, 0x10, 0x0F, 0x2E, 0x3F, 0x0D}, 16, 0},
{0xB9, (uint8_t []){0x23, 0x23}, 2, 0},
{0xBF, (uint8_t []){0x10, 0x14, 0x14, 0x0B, 0x0B, 0x0B}, 6, 0},
{0xF2, (uint8_t []){0x00}, 1, 0},
{0x73, (uint8_t []){0x04, 0xDA, 0x12, 0x54, 0x47}, 5, 0},
{0x77, (uint8_t []){0x6B, 0x5B, 0xFD, 0xC3, 0xC5}, 5, 0},
{0x7A, (uint8_t []){0x15, 0x27}, 2, 0},
{0x7B, (uint8_t []){0x04, 0x57}, 2, 0},
{0x7E, (uint8_t []){0x01, 0x0E}, 2, 0},
{0xBF, (uint8_t []){0x36}, 1, 0},
{0xE3, (uint8_t []){0x40, 0x40}, 2, 0},
{0xF0, (uint8_t []){0x00}, 1, 0},
{0xD0, (uint8_t []){0x00}, 1, 0},
{0x2A, (uint8_t []){0x00, 0x00, 0x01, 0x3F}, 4, 0},
{0x2B, (uint8_t []){0x00, 0x00, 0x01, 0xDF}, 4, 0},
{0x21, NULL, 0, 0},
{0x11, NULL, 0, 120},
{0x29, NULL, 0, 0},
{0x2C, NULL, 0, 0},
{0x3A, (uint8_t []){0x01}, 1, 0},
{0x36, (uint8_t []){0x00}, 1, 0},
{0x35, (uint8_t []){0x01}, 1, 20},
};
const st77922_lcd_init_cmd_t* st77922_board_init_commands(size_t* count) {
*count = sizeof(init_commands) / sizeof(init_commands[0]);
return init_commands;
}
@@ -0,0 +1,15 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once
#include <stddef.h>
#include <esp_lcd_st77922.h>
#ifdef __cplusplus
extern "C" {
#endif
const st77922_lcd_init_cmd_t* st77922_board_init_commands(size_t* count);
#ifdef __cplusplus
}
#endif
@@ -0,0 +1,207 @@
// SPDX-License-Identifier: Apache-2.0
#include <drivers/st77922_touch.h>
#include <st77922_module.h>
#include <tactility/check.h>
#include <tactility/device.h>
#include <tactility/driver.h>
#include <tactility/drivers/gpio_controller.h>
#include <tactility/drivers/i2c_controller.h>
#include <tactility/drivers/pointer.h>
#include <tactility/log.h>
#include <freertos/task.h>
#include <algorithm>
#include <cstdlib>
#define TAG "ST77922 touch"
#define GET_CONFIG(device) (static_cast<const St77922TouchConfig*>((device)->config))
static constexpr uint16_t REG_MAX_TOUCHES = 0x0009;
static constexpr uint16_t REG_TOUCH_INFO = 0x0010;
static constexpr uint16_t REG_TOUCH_POINT0 = 0x0014;
static constexpr uint8_t MAX_POINTS = 10;
static constexpr TickType_t TIMEOUT = pdMS_TO_TICKS(1000);
struct TouchPoint {
uint16_t x;
uint16_t y;
};
struct St77922TouchInternal {
Device* i2c;
GpioDescriptor* reset;
uint8_t supported_points;
uint8_t point_count;
TouchPoint points[MAX_POINTS];
bool swap_xy;
bool mirror_x;
bool mirror_y;
};
static error_t read_register(Device* i2c, uint16_t reg, uint8_t* data, size_t size, TickType_t timeout) {
const uint8_t address[] = {
static_cast<uint8_t>(reg >> 8),
static_cast<uint8_t>(reg & 0xFF),
};
return i2c_controller_write_read(i2c, 0x55, address, sizeof(address), data, size, timeout);
}
static error_t start(Device* device) {
auto* parent = device_get_parent(device);
check(device_get_type(parent) == &I2C_CONTROLLER_TYPE);
const auto* config = GET_CONFIG(device);
auto* internal = static_cast<St77922TouchInternal*>(calloc(1, sizeof(St77922TouchInternal)));
if (internal == nullptr) {
return ERROR_OUT_OF_MEMORY;
}
internal->i2c = parent;
internal->supported_points = 1;
internal->swap_xy = config->swap_xy;
internal->mirror_x = config->mirror_x;
internal->mirror_y = config->mirror_y;
if (config->pin_reset.gpio_controller != nullptr) {
internal->reset = gpio_descriptor_acquire(config->pin_reset.gpio_controller,
config->pin_reset.pin, GPIO_FLAG_DIRECTION_OUTPUT | GPIO_FLAG_ACTIVE_LOW, GPIO_OWNER_GPIO);
if (internal->reset == nullptr) {
free(internal);
return ERROR_RESOURCE;
}
if (gpio_descriptor_set_level(internal->reset, true) != ERROR_NONE) {
gpio_descriptor_release(internal->reset);
free(internal);
return ERROR_RESOURCE;
}
vTaskDelay(pdMS_TO_TICKS(10));
gpio_descriptor_set_level(internal->reset, false);
vTaskDelay(pdMS_TO_TICKS(100));
}
uint8_t supported = 0;
if (read_register(parent, REG_MAX_TOUCHES, &supported, 1, TIMEOUT) == ERROR_NONE
&& supported > 0 && supported <= MAX_POINTS) {
internal->supported_points = supported;
}
LOG_I(TAG, "Controller reports %u touch points", internal->supported_points);
device_set_driver_data(device, internal);
return ERROR_NONE;
}
static error_t stop(Device* device) {
auto* internal = static_cast<St77922TouchInternal*>(device_get_driver_data(device));
if (internal->reset != nullptr) {
gpio_descriptor_release(internal->reset);
}
free(internal);
device_set_driver_data(device, nullptr);
return ERROR_NONE;
}
static error_t enter_sleep(Device*) { return ERROR_NOT_SUPPORTED; }
static error_t exit_sleep(Device*) { return ERROR_NOT_SUPPORTED; }
static error_t read_data(Device* device, TickType_t timeout) {
auto* internal = static_cast<St77922TouchInternal*>(device_get_driver_data(device));
uint8_t touch_info = 0;
if (read_register(internal->i2c, REG_TOUCH_INFO, &touch_info, 1, timeout) != ERROR_NONE) {
internal->point_count = 0;
return ERROR_RESOURCE;
}
if ((touch_info & 0x08) == 0) {
internal->point_count = 0;
return ERROR_NONE;
}
uint8_t data[7 * MAX_POINTS] = {};
const size_t read_size = 7 * internal->supported_points;
if (read_register(internal->i2c, REG_TOUCH_POINT0, data, read_size, timeout) != ERROR_NONE) {
internal->point_count = 0;
return ERROR_RESOURCE;
}
internal->point_count = 0;
for (uint8_t index = 0; index < internal->supported_points; index++) {
const uint8_t offset = index * 7;
if ((data[offset] & 0x80) == 0) {
continue;
}
internal->points[internal->point_count++] = {
.x = static_cast<uint16_t>(((data[offset] & 0x3F) << 8) | data[offset + 1]),
.y = static_cast<uint16_t>(((data[offset + 2] & 0x3F) << 8) | data[offset + 3]),
};
}
return ERROR_NONE;
}
static bool get_touched_points(Device* device, uint16_t* x, uint16_t* y, uint16_t* strength,
uint8_t* count, uint8_t maximum) {
auto* internal = static_cast<St77922TouchInternal*>(device_get_driver_data(device));
const auto* config = GET_CONFIG(device);
*count = std::min(internal->point_count, maximum);
for (uint8_t index = 0; index < *count; index++) {
uint16_t point_x = internal->points[index].x;
uint16_t point_y = internal->points[index].y;
if (internal->swap_xy) {
std::swap(point_x, point_y);
}
const uint16_t max_x = internal->swap_xy ? config->y_max : config->x_max;
const uint16_t max_y = internal->swap_xy ? config->x_max : config->y_max;
x[index] = internal->mirror_x ? max_x - point_x : point_x;
y[index] = internal->mirror_y ? max_y - point_y : point_y;
if (strength != nullptr) {
strength[index] = 0;
}
}
return *count > 0;
}
static error_t set_swap_xy(Device* device, bool value) {
static_cast<St77922TouchInternal*>(device_get_driver_data(device))->swap_xy = value;
return ERROR_NONE;
}
static error_t get_swap_xy(Device* device, bool* value) {
*value = static_cast<St77922TouchInternal*>(device_get_driver_data(device))->swap_xy;
return ERROR_NONE;
}
static error_t set_mirror_x(Device* device, bool value) {
static_cast<St77922TouchInternal*>(device_get_driver_data(device))->mirror_x = value;
return ERROR_NONE;
}
static error_t get_mirror_x(Device* device, bool* value) {
*value = static_cast<St77922TouchInternal*>(device_get_driver_data(device))->mirror_x;
return ERROR_NONE;
}
static error_t set_mirror_y(Device* device, bool value) {
static_cast<St77922TouchInternal*>(device_get_driver_data(device))->mirror_y = value;
return ERROR_NONE;
}
static error_t get_mirror_y(Device* device, bool* value) {
*value = static_cast<St77922TouchInternal*>(device_get_driver_data(device))->mirror_y;
return ERROR_NONE;
}
static const PointerApi pointer_api = {
.enter_sleep = enter_sleep,
.exit_sleep = exit_sleep,
.read_data = read_data,
.get_touched_points = get_touched_points,
.set_swap_xy = set_swap_xy,
.get_swap_xy = get_swap_xy,
.set_mirror_x = set_mirror_x,
.get_mirror_x = get_mirror_x,
.set_mirror_y = set_mirror_y,
.get_mirror_y = get_mirror_y,
};
Driver st77922_touch_driver = {
.name = "st77922-touch",
.compatible = (const char*[]) { "sitronix,st77922-touch", nullptr },
.start_device = start,
.stop_device = stop,
.api = &pointer_api,
.device_type = &POINTER_TYPE,
.owner = &st77922_module,
.internal = nullptr
};
-147
View File
@@ -1,147 +0,0 @@
cmake_minimum_required(VERSION 3.20)
file(GLOB_RECURSE SOURCE_FILES "Source/*.c*")
get_filename_component(PROJECT_ROOT "${CMAKE_CURRENT_LIST_DIR}/.." ABSOLUTE)
# Get the project and device id
if (DEFINED ENV{ESP_IDF_VERSION})
include("../Buildscripts/device.cmake")
init_tactility_globals("../sdkconfig")
get_property(TACTILITY_DEVICE_PROJECT GLOBAL PROPERTY TACTILITY_DEVICE_PROJECT)
get_property(TACTILITY_DEVICE_ID GLOBAL PROPERTY TACTILITY_DEVICE_ID)
else ()
set(TACTILITY_DEVICE_ID simulator)
set(COMPONENT_LIB FirmwareSim)
set(TACTILITY_DEVICE_PROJECT Simulator)
endif ()
set(DEVICETREE_LOCATION "${PROJECT_ROOT}/Devices/${TACTILITY_DEVICE_ID}")
# Check if device has Bluetooth enabled
# Fixes the sdkconfig bluetooth enable options from getting nuked on non-P4+C6 builds when idf build runs
if (DEFINED ENV{ESP_IDF_VERSION})
file(READ "${DEVICETREE_LOCATION}/device.properties" device_properties_content)
if (device_properties_content MATCHES "hardware\\.bluetooth=true")
list(APPEND REQUIRES_LIST bt)
endif()
endif()
#
# DTS compiler python dependencies
#
execute_process(
COMMAND python -m pip install lark==1.3.1 pyyaml==6.0.3
WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}"
)
#
# Devicetree dependency collection
#
# REQUIRES_LIST below is computed once, at configure time. Without this, editing
# devicetree.yaml (e.g. adding a driver dependency) doesn't trigger a cmake reconfigure,
# so the new component's include dirs never reach the compiler until a fullclean.
set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS
"${DEVICETREE_LOCATION}/devicetree.yaml"
)
execute_process(
COMMAND python "${PROJECT_ROOT}/Buildscripts/DevicetreeCompiler/dependencies.py" "${DEVICETREE_LOCATION}"
WORKING_DIRECTORY "${PROJECT_ROOT}"
OUTPUT_VARIABLE DEVICE_DEPENDENCIES
OUTPUT_STRIP_TRAILING_WHITESPACE
)
# Tokenize to array of lines
separate_arguments(DEVICE_DEPENDENCIES UNIX_COMMAND "${DEVICE_DEPENDENCIES}")
#
# "Generated/" directory creation
#
set(GENERATED_DIR "${CMAKE_CURRENT_BINARY_DIR}/Generated")
# Ensure the directory is built in the correct CMake build phase
# If the check is not done, then another directory is created in the root of the build folder.
if (DEFINED CMAKE_CURRENT_BINARY_DIR)
file(MAKE_DIRECTORY "${GENERATED_DIR}")
endif ()
#
# Component
#
list(APPEND REQUIRES_LIST
Tactility
TactilityKernel
)
# Add devicetree dependencies
foreach(dts_dependency IN LISTS DEVICE_DEPENDENCIES)
message("Adding DTS dependency ${dts_dependency}")
list(APPEND REQUIRES_LIST ${dts_dependency})
endforeach()
if (DEFINED ENV{ESP_IDF_VERSION})
list(APPEND REQUIRES_LIST
TactilityC
)
idf_component_register(
SRCS ${SOURCE_FILES} "${GENERATED_DIR}/devicetree.c"
REQUIRES ${REQUIRES_LIST}
)
else ()
list(APPEND REQUIRES_LIST
Tactility
TactilityFreeRtos
lvgl-module
lvgl-window-manager-module
app-module
crypt-module
gps-module
http-module
gps-generic-module
gps-meshtastic-module
service-module
SDL2::SDL2-static
SDL2-static
)
add_executable(FirmwareSim ${SOURCE_FILES} "${GENERATED_DIR}/devicetree.c")
target_link_libraries(FirmwareSim PRIVATE ${REQUIRES_LIST})
endif ()
#
# Devicetree code generation
#
# A plain add_custom_command(OUTPUT ...) only reruns when its explicit DEPENDS (devicetree.yaml)
# changes, since ninja computes dirtiness up front, before any command in this invocation runs.
# The devicetree's real inputs span many files (dts, bindings yaml, driver headers) that a single
# DEPENDS can't enumerate, so this used to be forced to always-rerun via a separate "AlwaysRun"
# custom target that deleted devicetree.c first (DEPENDS on a *target* only creates an order-only
# ninja edge). That delete was invisible to ninja's DAG: on an up-to-date build, ninja would still
# run AlwaysRun (custom targets with no real output are always considered dirty) and delete the
# file, but *not* rerun the generation edge (its own explicit input hadn't changed) or recompile
# devicetree.c.obj (also considered clean) - silently leaving devicetree.c missing on disk until
# some later, unrelated build finally noticed and regenerated it. In between, any build that did
# need to (re)compile devicetree.c.obj hit "No such file or directory".
#
# add_custom_target(... COMMAND ...) has no such problem: unlike add_custom_command(OUTPUT ...),
# a custom target with a COMMAND always reruns on every ninja invocation, so generation and the
# stale artifact removal happen atomically in one edge. BYPRODUCTS tells ninja which files this
# target produces, so it still wires a proper (non-order-only) dependency for the sources that
# consume them.
add_custom_target(Generated ALL
COMMAND python "${CMAKE_SOURCE_DIR}/Buildscripts/DevicetreeCompiler/compile.py"
"${DEVICETREE_LOCATION}" "${GENERATED_DIR}"
BYPRODUCTS "${GENERATED_DIR}/devicetree.c" "${GENERATED_DIR}/devicetree.h"
WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}"
COMMENT "Generating devicetree source files..."
)
set_source_files_properties("${GENERATED_DIR}/devicetree.c" PROPERTIES GENERATED TRUE)
set_source_files_properties("${GENERATED_DIR}/devicetree.h" PROPERTIES GENERATED TRUE)
# Update target for generated code
target_sources(${COMPONENT_LIB} PRIVATE "${GENERATED_DIR}/devicetree.c")
target_include_directories(${COMPONENT_LIB} PRIVATE "${GENERATED_DIR}")
add_dependencies(${COMPONENT_LIB} Generated)
-22
View File
@@ -1,22 +0,0 @@
# ChangeLog
## v1.1.1 - 2025-06-26
* Added support for ESP32-C61
## v1.1.0 - 2025-05-06
* Added fast build for ELF application
* Added a script to generate the symbol table for the ELF APP:
* Supports generating symbols table based on ELF file
* Supports generating symbols table based on static libraries
## v1.0.0 - 2024-12-09
* Added support for the following RISC-V chips: ESP32-P4 and ESP32-C6
* Added support for linking other components to ELF file
* Fixed the issue of getting wrong symbol type
## v0.1.0 - 2023-08-14
* Add basic ELF loader component
-40
View File
@@ -1,40 +0,0 @@
if(CONFIG_ELF_LOADER)
set(srcs "src/esp_elf_symbol.c"
"src/esp_elf.c"
"src/esp_elf_adapter.c")
if(CONFIG_ELF_LOADER_CUSTOMER_SYMBOLS)
list(APPEND srcs "src/esp_all_symbol.c")
endif()
if(CONFIG_IDF_TARGET_ARCH_XTENSA)
list(APPEND srcs "src/arch/esp_elf_xtensa.c")
# ESP32-S2 need to set MMU to run ELF
if(CONFIG_IDF_TARGET_ESP32S2 AND (CONFIG_ELF_LOADER_LOAD_PSRAM))
list(APPEND srcs "src/soc/esp_elf_esp32s2.c")
endif()
elseif(CONFIG_IDF_TARGET_ARCH_RISCV)
list(APPEND srcs "src/arch/esp_elf_riscv.c")
endif()
set(include_dirs "include")
set(ldfragments "linker.lf")
endif()
if(CONFIG_IDF_TARGET_ESP32P4)
set(priv_req spi_flash esp_mm)
else()
set(priv_req spi_flash)
endif()
idf_component_register(SRCS ${srcs}
INCLUDE_DIRS ${include_dirs}
PRIV_REQUIRES spi_flash ${priv_req}
LDFRAGMENTS ${ldfragments})
include(package_manager)
if(CONFIG_ELF_LOADER)
cu_pkg_define_version(${CMAKE_CURRENT_LIST_DIR})
endif()
-53
View File
@@ -1,53 +0,0 @@
menu "Espressif ELF Loader Configuration"
visible if (IDF_TARGET_ESP32 || IDF_TARGET_ESP32S2 || IDF_TARGET_ESP32S3 || IDF_TARGET_ESP32C6 || IDF_TARGET_ESP32P4 || IDF_TARGET_ESP32C61)
config ELF_LOADER_BUS_ADDRESS_MIRROR
bool
default y if (IDF_TARGET_ESP32 || IDF_TARGET_ESP32S2 || IDF_TARGET_ESP32S3)
default n if (IDF_TARGET_ESP32C6 || IDF_TARGET_ESP32P4 || IDF_TARGET_ESP32C61)
config ELF_LOADER
bool "Enable Espressif ELF Loader"
default y
depends on (IDF_TARGET_ESP32 || IDF_TARGET_ESP32S2 || IDF_TARGET_ESP32S3 || IDF_TARGET_ESP32C6 || IDF_TARGET_ESP32P4 || IDF_TARGET_ESP32C61)
help
Select this option to enable ELF Loader and show the submenu with ELF Loader configuration choices.
if ELF_LOADER
config ELF_LOADER_CACHE_OFFSET
bool
default n
help
Select this option if D-cache and I-cache has different offset to access the same physical address.
config ELF_LOADER_SET_MMU
bool
default n
help
Select this option if D-cache and I-cache is not symmetry。
config ELF_LOADER_LOAD_PSRAM
bool "Load ELF to PSRAM"
default y
depends on (IDF_TARGET_ESP32S2 || IDF_TARGET_ESP32S3 || IDF_TARGET_ESP32P4 || IDF_TARGET_ESP32C61) && SPIRAM
select ELF_LOADER_CACHE_OFFSET if (IDF_TARGET_ESP32S2 || IDF_TARGET_ESP32S3)
select ELF_LOADER_SET_MMU if IDF_TARGET_ESP32S2
help
Load ELF file into PSRAM instead of internal SRAM.
menu "ELF Symbols Table"
config ELF_LOADER_LIBC_SYMBOLS
bool "Libc Symbols Table"
default y
config ELF_LOADER_ESPIDF_SYMBOLS
bool "ESP-IDF Symbols Table"
default y
config ELF_LOADER_CUSTOMER_SYMBOLS
bool "Customer Symbols Table"
default n
endmenu
endif
endmenu
-109
View File
@@ -1,109 +0,0 @@
## Description
[![Component Registry](https://components.espressif.com/components/espressif/elf_loader/badge.svg)](https://components.espressif.com/components/espressif/elf_loader)
Espressif ELF(Executable and Linkable Format) loader is a software development kit that is developed based on the ESP-IDF, mainly used to load ELF file compiled based on ESP32 series SoCs to the executable memory area, then link and execute it.
In this way, the application does not need compile into the whole firmware in advance. It is like running the compiled program through terminal input `./main.o` on the Ubuntu platform automatically, which realizes the separation of application and kernel.
This ELF loader supports following SoCs:
- ESP32
- ESP32-S2, support running ELF in PSRAM
- ESP32-S3, support running ELF in PSRAM
- ESP32-P4, support running ELF in PSRAM
- ESP32-C6
- ESP32-C61, support running ELF in PSRAM
### Usage
#### Firmware
Add a dependency on this component in your component or project's idf_component.yml file.
```yml
dependencies:
espressif/elf_loader: "1.*"
```
Enable ELF loader in the menuconfig:
```
Component config --->
ESP-ELFLoader Configuration --->
[*] Enable Espressif ELF Loader
```
Add API calls in your project as follows:
```c
#include "esp_elf.h"
esp_elf_t elf;
// Your Code
esp_elf_init(&elf);
esp_elf_relocate(&elf, elf_file_data_bytes);
esp_elf_request(&elf, 0, argc, argv);
esp_elf_deinit(&elf);
```
#### ELF APP
To use this feature to compile ELF file, including the required CMake file in your project's CMakeLists.txt file after the line project(XXXX).
```cmake
project(XXXX)
# Add
include(elf_loader)
project_elf(XXXX)
```
Build the project as an ordinary ESP-IDF project, and then the ELF file named `XXXX.app.elf` is in the build directory.
### ELF APP Fast Build
Users can enable ELF fast build functionality by configuring CMAKE's generator as Unit Makefile. The reference command is as follows:
```bash
idf.py -G 'Unix Makefiles' set-target <chip-name>
```
Then input the ELF APP build command as follows:
```
idf.py elf
```
The build system will only build ELF target components and show the following logs:
```
Building C object esp-idf/main/CMakeFiles/__idf_main.dir/main.c.obj
Linking C static library libmain.a
Build ELF: hello_world.app.elf
Built target elf
```
### Adding the Component to Your Project
Please use the component manager command add-dependency to add the elf_loader component as a dependency in your project. During the CMake step, the component will be downloaded automatically.
```
idf.py add-dependency "espressif/elf_loader=*"
```
### Examples
Please use the component manager command create-project-from-example to create a project from the example template.
```
idf.py create-project-from-example "espressif/elf_loader=*:elf_loader_example"
```
This command will download the example elf_loader_example into the current folder. You can navigate into it to build and flash the example.
Alternatively, you can download examples from the esp-iot-solution repository:
1. [build_elf_file_example](https://github.com/espressif/esp-iot-solution/tree/master/examples/elf_loader/build_elf_file_example)
2. [elf_loader_example](https://github.com/espressif/esp-iot-solution/tree/master/examples/elf_loader/elf_loader_example)
-79
View File
@@ -1,79 +0,0 @@
# The script is to generate ELF for application
# Trick to temporarily redefine project(). When functions are overridden in CMake, the originals can still be accessed
# using an underscore prefixed function of the same name. The following lines make sure that project calls
# the original project(). See https://cmake.org/pipermail/cmake/2015-October/061751.html.
function(project_elf)
endfunction()
function(_project_elf)
endfunction()
macro(project_elf project_name)
# Enable these options to remove unused symbols and reduce linked objects
set(cflags -nostartfiles
-nostdlib
-fPIC
-shared
-e app_main
-fdata-sections
-ffunction-sections
-Wl,--gc-sections
-fvisibility=hidden)
# Enable this options to remove unnecessary sections in
list(APPEND cflags -Wl,--strip-all
-Wl,--strip-debug
-Wl,--strip-discarded)
list(APPEND cflags -Dmain=app_main)
idf_build_set_property(COMPILE_OPTIONS "${cflags}" APPEND)
set(elf_app "${CMAKE_PROJECT_NAME}.app.elf")
# Remove more unused sections
string(REPLACE "-elf-gcc" "-elf-strip" ${CMAKE_STRIP} ${CMAKE_C_COMPILER})
set(strip_flags --strip-unneeded
--remove-section=.comment
--remove-section=.got.loc
--remove-section=.dynamic)
if(CONFIG_IDF_TARGET_ARCH_XTENSA)
list(APPEND strip_flags --remove-section=.xt.lit
--remove-section=.xt.prop
--remove-section=.xtensa.info)
elseif(CONFIG_IDF_TARGET_ARCH_RISCV)
list(APPEND strip_flags --remove-section=.riscv.attributes)
endif()
# Link input list of libraries to ELF
list(PREPEND ELF_COMPONENTS "main")
if(ELF_COMPONENTS)
foreach(c ${ELF_COMPONENTS})
list(APPEND elf_libs "esp-idf/${c}/lib${c}.a")
if(${CMAKE_GENERATOR} STREQUAL "Unix Makefiles")
add_custom_command(OUTPUT elf_${c}_app
COMMAND +${CMAKE_MAKE_PROGRAM} "__idf_${c}/fast"
COMMENT "Build Component: ${c}"
)
list(APPEND elf_dependeces "elf_${c}_app")
else()
list(APPEND elf_dependeces "idf::${c}")
endif()
endforeach()
endif()
if (ELF_LIBS)
list(APPEND elf_libs "${ELF_LIBS}")
endif()
spaces2list(elf_libs)
add_custom_command(OUTPUT elf_app
COMMAND ${CMAKE_C_COMPILER} ${cflags} ${elf_libs} -o ${elf_app}
COMMAND ${CMAKE_STRIP} ${strip_flags} ${elf_app}
DEPENDS ${elf_dependeces}
COMMENT "Build ELF: ${elf_app}"
)
add_custom_target(elf ALL DEPENDS elf_app)
endmacro()
-19
View File
@@ -1,19 +0,0 @@
version: "1.1.1"
targets:
- esp32
- esp32s2
- esp32s3
- esp32c6
- esp32c61
- esp32p4
description: Espressif ELF(Executable and Linkable Format) Loader
url: https://github.com/espressif/esp-iot-solution/tree/master/components/elf_loader
dependencies:
idf: ">=4.4.3"
espressif/cmake_utilities: "0.*"
examples:
- path: ../../examples/elf_loader/build_elf_file_example
- path: ../../examples/elf_loader/elf_loader_example
sbom:
supplier: 'Organization: Espressif Systems (Shanghai) CO LTD'
originator: 'Organization: Espressif Systems (Shanghai) CO LTD'
-103
View File
@@ -1,103 +0,0 @@
/*
* SPDX-FileCopyrightText: 2023-2024 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#pragma once
#include "private/elf_types.h"
#ifdef __cplusplus
extern "C" {
#endif
/**
* @brief Map symbol's address of ELF to physic space.
*
* @param elf - ELF object pointer
* @param sym - ELF symbol address
*
* @return Mapped physic address.
*/
uintptr_t esp_elf_map_sym(esp_elf_t *elf, uintptr_t sym);
/**
* @brief Initialize ELF object.
*
* @param elf - ELF object pointer
*
* @return ESP_OK if success or other if failed.
*/
int esp_elf_init(esp_elf_t *elf);
/**
* @brief Decode and relocate ELF data.
*
* @param elf - ELF object pointer
* @param pbuf - ELF data buffer
*
* @return ESP_OK if success or other if failed.
*/
int esp_elf_relocate(esp_elf_t *elf, const uint8_t *pbuf);
/**
* @brief Request running relocated ELF function.
*
* @param elf - ELF object pointer
* @param opt - Request options
* @param argc - Arguments number
* @param argv - Arguments value array
*
* @return ESP_OK if success or other if failed.
*/
int esp_elf_request(esp_elf_t *elf, int opt, int argc, char *argv[]);
/**
* @brief Deinitialize ELF object.
*
* @param elf - ELF object pointer
*
* @return None
*/
void esp_elf_deinit(esp_elf_t *elf);
/**
* @brief Print header description information of ELF.
*
* @param pbuf - ELF data buffer
*
* @return None
*/
void esp_elf_print_ehdr(const uint8_t *pbuf);
/**
* @brief Print program header description information of ELF.
*
* @param pbuf - ELF data buffer
*
* @return None
*/
void esp_elf_print_phdr(const uint8_t *pbuf);
/**
* @brief Print section header description information of ELF.
*
* @param pbuf - ELF data buffer
*
* @return None
*/
void esp_elf_print_shdr(const uint8_t *pbuf);
/**
* @brief Print section information of ELF.
*
* @param pbuf - ELF data buffer
*
* @return None
*/
void esp_elf_print_sec(esp_elf_t *elf);
#ifdef __cplusplus
}
#endif
@@ -1,99 +0,0 @@
/*
* SPDX-FileCopyrightText: 2023 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#pragma once
#include "private/elf_types.h"
#ifdef __cplusplus
extern "C" {
#endif
/* Notes: align_size needs to be a power of 2 */
#define ELF_ALIGN(_a, align_size) (((_a) + (align_size - 1)) & \
~(align_size - 1))
/**
* @brief Allocate block of memory.
*
* @param n - Memory size in byte
* @param exec - True: memory can run executable code; False: memory can R/W data
*
* @return Memory pointer if success or NULL if failed.
*/
void *esp_elf_malloc(uint32_t n, bool exec);
/**
* @brief Free block of memory.
*
* @param ptr - memory block pointer allocated by "esp_elf_malloc"
*
* @return None
*/
void esp_elf_free(void *ptr);
/**
* @brief Relocates target architecture symbol of ELF
*
* @param elf - ELF object pointer
* @param rela - Relocated symbol data
* @param sym - ELF symbol table
* @param addr - Jumping target address
*
* @return ESP_OK if success or other if failed.
*/
int esp_elf_arch_relocate(esp_elf_t *elf, const elf32_rela_t *rela,
const elf32_sym_t *sym, uint32_t addr);
/**
* @brief Remap symbol from ".data" to ".text" section.
*
* @param elf - ELF object pointer
* @param sym - ELF symbol table
*
* @return Remapped symbol value
*/
#ifdef CONFIG_ELF_LOADER_CACHE_OFFSET
uintptr_t elf_remap_text(esp_elf_t *elf, uintptr_t sym);
#endif
/**
* @brief Flush data from cache to external RAM.
*
* @param None
*
* @return None
*/
#ifdef CONFIG_ELF_LOADER_LOAD_PSRAM
void esp_elf_arch_flush(void);
#endif
/**
* @brief Initialize MMU hardware remapping function.
*
* @param elf - ELF object pointer
*
* @return 0 if success or a negative value if failed.
*/
#ifdef CONFIG_ELF_LOADER_SET_MMU
int esp_elf_arch_init_mmu(esp_elf_t *elf);
#endif
/**
* @brief De-initialize MMU hardware remapping function.
*
* @param elf - ELF object pointer
*
* @return None
*/
#ifdef CONFIG_ELF_LOADER_SET_MMU
void esp_elf_arch_deinit_mmu(esp_elf_t *elf);
#endif
#ifdef __cplusplus
}
#endif
@@ -1,54 +0,0 @@
/*
* SPDX-FileCopyrightText: 2023 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#pragma once
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
#define ESP_ELFSYM_EXPORT(_sym) { #_sym, (void*)&_sym }
#define ESP_ELFSYM_END { NULL, NULL }
/** @brief Function symbol description */
struct esp_elfsym {
const char *name; /*!< Function name */
const void *sym; /*!< Function pointer */
};
/**
* @brief Find symbol address by name.
*
* @param sym_name - Symbol name
*
* @return Symbol address if success or 0 if failed.
*/
uintptr_t elf_find_sym(const char *sym_name);
/**
* @brief Resolves a symbol name (e.g. function name) to its address.
*
* @param sym_name - Symbol name
* @return Symbol address if success or 0 if failed.
*/
typedef uintptr_t (*symbol_resolver)(const char *sym_name);
/**
* @brief Override the internal symbol resolver.
* The default resolver is based on static lists that are determined by KConfig.
* This override allows for an arbitrary implementation.
*
* @param resolver the resolver function
*/
void elf_set_symbol_resolver(symbol_resolver resolver);
#ifdef __cplusplus
}
#endif
@@ -1,247 +0,0 @@
/*
* SPDX-FileCopyrightText: 2023 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#pragma once
#include <unistd.h>
#include <stdint.h>
#include <stddef.h>
#include <stdbool.h>
#include "sdkconfig.h"
#ifdef __cplusplus
extern "C" {
#endif
#define EI_NIDENT 16 /*!< Magic number and other information length */
/** @brief Type of segment */
#define PT_NULL 0 /*!< Program header table entry unused */
#define PT_LOAD 1 /*!< Loadable program segment */
#define PT_DYNAMIC 2 /*!< Dynamic linking information */
#define PT_INTERP 3 /*!< Program interpreter */
#define PT_NOTE 4 /*!< Auxiliary information */
#define PT_SHLIB 5 /*!< Reserved */
#define PT_PHDR 6 /*!< Entry for header table itself */
#define PT_TLS 7 /*!< Thread-local storage segment */
#define PT_NUM 8 /*!< Number of defined types */
#define PT_LOOS 0x60000000 /*!< Start of OS-specific */
#define PT_GNU_EH_FRAME 0x6474e550 /*!< GCC .eh_frame_hdr segment */
#define PT_GNU_STACK 0x6474e551 /*!< Indicates stack executability */
#define PT_GNU_RELRO 0x6474e552 /*!< Read-only after relocation */
#define PT_LOSUNW 0x6ffffffa
#define PT_SUNWBSS 0x6ffffffa /*!< Sun Specific segment */
#define PT_SUNWSTACK 0x6ffffffb /*!< Stack segment */
#define PT_HISUNW 0x6fffffff
#define PT_HIOS 0x6fffffff /*!< End of OS-specific */
#define PT_LOPROC 0x70000000 /*!< Start of processor-specific */
#define PT_HIPROC 0x7fffffff /*!< End of processor-specific */
/** @brief Section Type */
#define SHT_NULL 0 /*!< invalid section header */
#define SHT_PROGBITS 1 /*!< some useful section like .text, .data .got, .plt .rodata .interp and so on */
#define SHT_SYMTAB 2 /*!< symbol table */
#define SHT_STRTAB 3 /*!< string table */
#define SHT_RELA 4 /*!< relocation table */
#define SHT_HASH 5 /*!< HASH table */
#define SHT_SYNAMIC 6 /*!< dynamic symbol table */
#define SHT_NOTE 7 /*!< note information */
#define SHT_NOBITS 8 /*!< .bss */
#define SHT_REL 9 /*!< relocation table */
#define SHT_SHKIB 10 /*!< reserved but has unspecified semantics. */
#define SHT_SYNSYM 11 /*!< dynamic symbol */
#define SHT_LOPROC 0x70000000 /*!< reserved for processor-specific semantics */
#define SHT_LOUSER 0x7fffffff /*!< lower bound of the range of indexes reserved for application programs */
#define SHT_HIUSER 0xffffffff /*!< upper bound of the range of indexes reserved for application programs. */
/** @brief Section Attribute Flags */
#define SHF_WRITE 1 /*!< writable when task runs */
#define SHF_ALLOC 2 /*!< allocated when task runs */
#define SHF_EXECINSTR 4 /*!< machine code */
#define SHF_MASKPROG 0xf0000000 /*!< reserved for processor-specific semantics */
/** @brief Symbol Types */
#define STT_NOTYPE 0 /*!< symbol type is unspecified */
#define STT_OBJECT 1 /*!< data object */
#define STT_FUNC 2 /*!< code object */
#define STT_SECTION 3 /*!< symbol identifies an ELF section */
#define STT_FILE 4 /*!< symbol's name is file name */
#define STT_COMMON 5 /*!< common data object */
#define STT_TLS 6 /*!< thread-local data object */
#define STT_NUM 7 /*!< defined types in generic range */
#define STT_LOOS 10 /*!< Low OS specific range */
#define STT_HIOS 12 /*!< High OS specific range */
#define STT_LOPROC 13 /*!< processor specific range */
#define STT_HIPROC 15 /*!< processor specific link range */
/** @brief Section names */
#define ELF_BSS ".bss" /*!< uninitialized data */
#define ELF_DATA ".data" /*!< initialized data */
#define ELF_DEBUG ".debug" /*!< debug */
#define ELF_DYNAMIC ".dynamic" /*!< dynamic linking information */
#define ELF_DYNSTR ".dynstr" /*!< dynamic string table */
#define ELF_DYNSYM ".dynsym" /*!< dynamic symbol table */
#define ELF_FINI ".fini" /*!< termination code */
#define ELF_GOT ".got" /*!< global offset table */
#define ELF_HASH ".hash" /*!< symbol hash table */
#define ELF_INIT ".init" /*!< initialization code */
#define ELF_REL_DATA ".rel.data" /*!< relocation data */
#define ELF_REL_FINI ".rel.fini" /*!< relocation termination code */
#define ELF_REL_INIT ".rel.init" /*!< relocation initialization code */
#define ELF_REL_DYN ".rel.dyn" /*!< relocaltion dynamic link info */
#define ELF_REL_RODATA ".rel.rodata" /*!< relocation read-only data */
#define ELF_REL_TEXT ".rel.text" /*!< relocation code */
#define ELF_RODATA ".rodata" /*!< read-only data */
#define ELF_SHSTRTAB ".shstrtab" /*!< section header string table */
#define ELF_STRTAB ".strtab" /*!< string table */
#define ELF_SYMTAB ".symtab" /*!< symbol table */
#define ELF_TEXT ".text" /*!< code */
#define ELF_DATA_REL_RO ".data.rel.ro" /*!< dynamic read-only data */
#define ELF_PLT ".plt" /*!< procedure linkage table. */
#define ELF_GOT_PLT ".got.plt" /*!< a table where resolved addresses from external functions are stored */
/** @brief ELF section and symbol operation */
#define ELF_SEC_TEXT 0
#define ELF_SEC_BSS 1
#define ELF_SEC_DATA 2
#define ELF_SEC_RODATA 3
#define ELF_SEC_DRLRO 4
#define ELF_SECS 5
#define ELF_ST_BIND(_i) ((_i) >> 4)
#define ELF_ST_TYPE(_i) ((_i) & 0xf)
#define ELF_ST_INFO(_b, _t) (((_b)<<4) + ((_t) & 0xf))
#define ELF_R_SYM(_i) ((_i) >> 8)
#define ELF_R_TYPE(_i) ((unsigned char)(_i))
#define ELF_R_INFO(_s, _t) (((_s) << 8) + (unsigned char)(_t))
#define ELF_SEC_MAP(_elf, _sec, _addr) \
((_elf)->sec[(_sec)].addr - \
(_elf)->sec[(_sec)].v_addr + \
(_addr))
typedef unsigned int Elf32_Addr;
typedef unsigned int Elf32_Off;
typedef unsigned int Elf32_Word;
typedef unsigned short Elf32_Half;
typedef int Elf32_Sword;
/** @brief ELF Header */
typedef struct elf32_hdr {
unsigned char ident[EI_NIDENT]; /*!< ELF Identification */
Elf32_Half type; /*!< object file type */
Elf32_Half machine; /*!< machine */
Elf32_Word version; /*!< object file version */
Elf32_Addr entry; /*!< virtual entry point */
Elf32_Off phoff; /*!< program header table offset */
Elf32_Off shoff; /*!< section header table offset */
Elf32_Word flags; /*!< processor-specific flags */
Elf32_Half ehsize; /*!< ELF header size */
Elf32_Half phentsize; /*!< program header entry size */
Elf32_Half phnum; /*!< number of program header entries */
Elf32_Half shentsize; /*!< section header entry size */
Elf32_Half shnum; /*!< number of section header entries */
Elf32_Half shstrndx; /*!< section header table's "section header string table" entry offset */
} elf32_hdr_t;
/** @brief Program Header */
typedef struct elf32_phdr {
Elf32_Word type; /* segment type */
Elf32_Off offset; /* segment offset */
Elf32_Addr vaddr; /* virtual address of segment */
Elf32_Addr paddr; /* physical address - ignored? */
Elf32_Word filesz; /* number of bytes in file for seg. */
Elf32_Word memsz; /* number of bytes in mem. for seg. */
Elf32_Word flags; /* flags */
Elf32_Word align; /* memory alignment */
} elf32_phdr_t;
/** @brief Section Header */
typedef struct elf32_shdr {
Elf32_Word name; /*!< section index and it is offset in section .shstrtab */
Elf32_Word type; /*!< type */
Elf32_Word flags; /*!< flags */
Elf32_Addr addr; /*!< start address when map to task space */
Elf32_Off offset; /*!< offset address from file start address */
Elf32_Word size; /*!< size */
Elf32_Word link; /*!< link to another section */
Elf32_Word info; /*!< additional section information */
Elf32_Word addralign; /*!< address align */
Elf32_Word entsize; /*!< index table size */
} elf32_shdr_t;
/** @brief Symbol Table Entry */
typedef struct elf32_sym {
Elf32_Word name; /*!< name - index into string table */
Elf32_Addr value; /*!< symbol value */
Elf32_Word size; /*!< symbol size */
unsigned char info; /*!< type and binding */
unsigned char other; /*!< 0 - no defined meaning */
Elf32_Half shndx; /*!< section header index */
} elf32_sym_t;
/** @brief Relocation entry with implicit addend */
typedef struct elf32_rel {
Elf32_Addr offset; /*!< offset of relocation */
Elf32_Word info; /*!< symbol table index and type */
} elf32_rel_t;
/** @brief Relocation entry with explicit addend */
typedef struct elf32_rela {
Elf32_Addr offset; /*!< offset of relocation */
Elf32_Word info; /*!< symbol table index and type */
Elf32_Sword addend; /*!< Added information */
} elf32_rela_t;
/** @brief ELF section object */
typedef struct esp_elf_sec {
uintptr_t v_addr; /*!< symbol virtual address */
off_t offset; /*!< offset in ELF */
uintptr_t addr; /*!< section physic address in memory */
size_t size; /*!< section size */
} esp_elf_sec_t;
/** @brief ELF object */
typedef struct esp_elf {
unsigned char *psegment; /*!< segment buffer pointer */
uint32_t svaddr; /*!< start virtual address of segment */
unsigned char *ptext; /*!< instruction buffer pointer */
unsigned char *pdata; /*!< data buffer pointer */
esp_elf_sec_t sec[ELF_SECS]; /*!< ".bss", "data", "rodata", ".text" */
int (*entry)(int argc, char *argv[]); /*!< Entry pointer of ELF */
#ifdef CONFIG_ELF_LOADER_SET_MMU
uint32_t text_off; /* .text symbol offset */
uint32_t mmu_off; /* MMU unit offset */
uint32_t mmu_num; /* MMU unit total number */
#endif
} esp_elf_t;
#ifdef __cplusplus
}
#endif
-202
View File
@@ -1,202 +0,0 @@
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.
-14
View File
@@ -1,14 +0,0 @@
[sections:elf_loader_got_plt]
entries:
.got.plt
.got
[scheme:elf_loader_default]
entries:
elf_loader_got_plt -> flash_rodata
[mapping:elf_loader]
archive: *
entries:
* (elf_loader_default);
elf_loader_got_plt -> flash_rodata KEEP() SURROUND(_esp_elf_loader_got_plt)
@@ -1 +0,0 @@
set(CMAKE_MODULE_PATH ${CMAKE_CURRENT_LIST_DIR} ${CMAKE_MODULE_PATH})
@@ -1,112 +0,0 @@
/*
* SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#include <assert.h>
#include <sys/errno.h>
#include "esp_elf.h"
#include "esp_log.h"
#include "private/elf_platform.h"
/** @brief RISC-V relocations defined by the ABIs */
#define R_RISCV_NONE 0
#define R_RISCV_32 1
#define R_RISCV_64 2
#define R_RISCV_RELATIVE 3
#define R_RISCV_COPY 4
#define R_RISCV_JUMP_SLOT 5
#define R_RISCV_TLS_DTPMOD32 6
#define R_RISCV_TLS_DTPMOD64 7
#define R_RISCV_TLS_DTPREL32 8
#define R_RISCV_TLS_DTPREL64 9
#define R_RISCV_TLS_TPREL32 10
#define R_RISCV_TLS_TPREL64 11
#define R_RISCV_TLS_DESC 12
#define R_RISCV_BRANCH 16
#define R_RISCV_JAL 17
#define R_RISCV_CALL 18
#define R_RISCV_CALL_PLT 19
#define R_RISCV_GOT_HI20 20
#define R_RISCV_TLS_GOT_HI20 21
#define R_RISCV_TLS_GD_HI20 22
#define R_RISCV_PCREL_HI20 23
#define R_RISCV_PCREL_LO12_I 24
#define R_RISCV_PCREL_LO12_S 25
#define R_RISCV_HI20 26
#define R_RISCV_LO12_I 27
#define R_RISCV_LO12_S 28
#define R_RISCV_TPREL_HI20 29
#define R_RISCV_TPREL_LO12_I 30
#define R_RISCV_TPREL_LO12_S 31
#define R_RISCV_TPREL_ADD 32
#define R_RISCV_ADD8 33
#define R_RISCV_ADD16 34
#define R_RISCV_ADD32 35
#define R_RISCV_ADD64 36
#define R_RISCV_SUB8 37
#define R_RISCV_SUB16 38
#define R_RISCV_SUB32 39
#define R_RISCV_SUB64 40
#define R_RISCV_GNU_VTINHERIT 41
#define R_RISCV_GNU_VTENTRY 42
#define R_RISCV_ALIGN 43
#define R_RISCV_RVC_BRANCH 44
#define R_RISCV_RVC_JUMP 45
#define R_RISCV_RVC_LUI 46
#define R_RISCV_RELAX 51
#define R_RISCV_SUB6 52
#define R_RISCV_SET6 53
#define R_RISCV_SET8 54
#define R_RISCV_SET16 55
#define R_RISCV_SET32 56
#define R_RISCV_32_PCREL 57
#define R_RISCV_IRELATIVE 58
#define R_RISCV_PLT32 59
static const char *TAG = "elf_arch";
/**
* @brief Relocates target architecture symbol of ELF
*
* @param elf - ELF object pointer
* @param rela - Relocated symbol data
* @param sym - ELF symbol table
* @param addr - Jumping target address
*
* @return ESP_OK if success or other if failed.
*/
int esp_elf_arch_relocate(esp_elf_t *elf, const elf32_rela_t *rela,
const elf32_sym_t *sym, uint32_t addr)
{
uint32_t *where;
assert(elf && rela);
where = (uint32_t *)((uint8_t *)elf->psegment + rela->offset + elf->svaddr);
ESP_LOGD(TAG, "type: %d, where=%p addr=0x%x offset=0x%x",
ELF_R_TYPE(rela->info), where, (int)elf->psegment, (int)rela->offset);
/* Do relocation based on relocation type */
switch (ELF_R_TYPE(rela->info)) {
case R_RISCV_NONE:
break;
case R_RISCV_32:
*where = addr + rela->addend;
break;
case R_RISCV_RELATIVE:
*where = (Elf32_Addr)((uint8_t *)elf->psegment - elf->svaddr + rela->addend);
break;
case R_RISCV_JUMP_SLOT:
*where = addr;
break;
default:
ESP_LOGE(TAG, "info=%d is not supported\n", ELF_R_TYPE(rela->info));
return -EINVAL;
}
return 0;
}
@@ -1,113 +0,0 @@
/*
* SPDX-FileCopyrightText: 2023-2024 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#include <assert.h>
#include <sys/errno.h>
#include "esp_elf.h"
#include "esp_log.h"
#include "private/elf_platform.h"
/** @brief Xtensa relocations defined by the ABIs */
#define R_XTENSA_NONE 0
#define R_XTENSA_32 1
#define R_XTENSA_RTLD 2
#define R_XTENSA_GLOB_DAT 3
#define R_XTENSA_JMP_SLOT 4
#define R_XTENSA_RELATIVE 5
#define R_XTENSA_PLT 6
#define R_XTENSA_OP0 8
#define R_XTENSA_OP1 9
#define R_XTENSA_OP2 10
#define R_XTENSA_ASM_EXPAND 11
#define R_XTENSA_ASM_SIMPLIFY 12
#define R_XTENSA_GNU_VTINHERIT 15
#define R_XTENSA_GNU_VTENTRY 16
#define R_XTENSA_DIFF8 17
#define R_XTENSA_DIFF16 18
#define R_XTENSA_DIFF32 19
#define R_XTENSA_SLOT0_OP 20
#define R_XTENSA_SLOT1_OP 21
#define R_XTENSA_SLOT2_OP 22
#define R_XTENSA_SLOT3_OP 23
#define R_XTENSA_SLOT4_OP 24
#define R_XTENSA_SLOT5_OP 25
#define R_XTENSA_SLOT6_OP 26
#define R_XTENSA_SLOT7_OP 27
#define R_XTENSA_SLOT8_OP 28
#define R_XTENSA_SLOT9_OP 29
#define R_XTENSA_SLOT10_OP 30
#define R_XTENSA_SLOT11_OP 31
#define R_XTENSA_SLOT12_OP 32
#define R_XTENSA_SLOT13_OP 33
#define R_XTENSA_SLOT14_OP 34
#define R_XTENSA_SLOT0_ALT 35
#define R_XTENSA_SLOT1_ALT 36
#define R_XTENSA_SLOT2_ALT 37
#define R_XTENSA_SLOT3_ALT 38
#define R_XTENSA_SLOT4_ALT 39
#define R_XTENSA_SLOT5_ALT 40
#define R_XTENSA_SLOT6_ALT 41
#define R_XTENSA_SLOT7_ALT 42
#define R_XTENSA_SLOT8_ALT 43
#define R_XTENSA_SLOT9_ALT 44
#define R_XTENSA_SLOT10_ALT 45
#define R_XTENSA_SLOT11_ALT 46
#define R_XTENSA_SLOT12_ALT 47
#define R_XTENSA_SLOT13_ALT 48
#define R_XTENSA_SLOT14_ALT 49
static const char *TAG = "elf_arch";
/**
* @brief Relocates target architecture symbol of ELF
*
* @param elf - ELF object pointer
* @param rela - Relocated symbol data
* @param sym - ELF symbol table
* @param addr - Jumping target address
*
* @return ESP_OK if success or other if failed.
*/
int esp_elf_arch_relocate(esp_elf_t *elf, const elf32_rela_t *rela,
const elf32_sym_t *sym, uint32_t addr)
{
uint32_t val;
uint32_t *where;
assert(elf && rela);
where = (uint32_t *)esp_elf_map_sym(elf, rela->offset);
ESP_LOGD(TAG, "type: %d, where=%p addr=0x%x offset=0x%x\n",
ELF_R_TYPE(rela->info), where, (int)addr, (int)rela->offset);
switch (ELF_R_TYPE(rela->info)) {
case R_XTENSA_RELATIVE:
val = esp_elf_map_sym(elf, *where);
#ifdef CONFIG_ELF_LOADER_CACHE_OFFSET
*where = elf_remap_text(elf, val);
#else
*where = val;
#endif
break;
case R_XTENSA_RTLD:
break;
case R_XTENSA_GLOB_DAT:
case R_XTENSA_JMP_SLOT:
#ifdef CONFIG_ELF_LOADER_CACHE_OFFSET
*where = elf_remap_text(elf, addr);
#else
*where = addr;
#endif
break;
default:
ESP_LOGE(TAG, "info=%d is not supported", ELF_R_TYPE(rela->info));
return -EINVAL;
}
return 0;
}
-21
View File
@@ -1,21 +0,0 @@
/*
* SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#include <stddef.h>
#include "private/elf_symbol.h"
/* Extern declarations from ELF symbol table */
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wbuiltin-declaration-mismatch"
#pragma GCC diagnostic pop
/* Available ELF symbols table: g_customer_elfsyms */
const struct esp_elfsym g_customer_elfsyms[] = {
ESP_ELFSYM_END
};
-676
View File
@@ -1,676 +0,0 @@
/*
* SPDX-FileCopyrightText: 2023-2024 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/errno.h>
#include <sys/param.h>
#include "esp_log.h"
#include "soc/soc_caps.h"
#if SOC_CACHE_INTERNAL_MEM_VIA_L1CACHE
#include "hal/cache_ll.h"
#endif
#include "private/elf_symbol.h"
#include "private/elf_platform.h"
#include "esp_elf.h"
#define stype(_s, _t) ((_s)->type == (_t))
#define sflags(_s, _f) (((_s)->flags & (_f)) == (_f))
#define ADDR_OFFSET (0x400)
uintptr_t elf_find_sym_default(const char *sym_name);
static const char *TAG = "ELF";
static symbol_resolver current_resolver = elf_find_sym_default;
/**
* @brief Find symbol address by name.
*
* @param sym_name - Symbol name
*
* @return Symbol address if success or 0 if failed.
*/
uintptr_t elf_find_sym(const char *sym_name) {
return current_resolver(sym_name);
}
#if CONFIG_ELF_LOADER_BUS_ADDRESS_MIRROR
/**
* @brief Load ELF section.
*
* @param elf - ELF object pointer
* @param pbuf - ELF data buffer
*
* @return ESP_OK if success or other if failed.
*/
static int esp_elf_load_section(esp_elf_t *elf, const uint8_t *pbuf)
{
uint32_t entry;
uint32_t size;
const elf32_hdr_t *ehdr = (const elf32_hdr_t *)pbuf;
const elf32_shdr_t *shdr = (const elf32_shdr_t *)(pbuf + ehdr->shoff);
const char *shstrab = (const char *)pbuf + shdr[ehdr->shstrndx].offset;
/* Calculate ELF image size */
for (uint32_t i = 0; i < ehdr->shnum; i++) {
const char *name = shstrab + shdr[i].name;
if (stype(&shdr[i], SHT_PROGBITS) && sflags(&shdr[i], SHF_ALLOC)) {
if (sflags(&shdr[i], SHF_EXECINSTR) && !strcmp(ELF_TEXT, name)) {
ESP_LOGD(TAG, ".text sec addr=0x%08x size=0x%08x offset=0x%08x",
shdr[i].addr, shdr[i].size, shdr[i].offset);
elf->sec[ELF_SEC_TEXT].v_addr = shdr[i].addr;
elf->sec[ELF_SEC_TEXT].size = ELF_ALIGN(shdr[i].size, 4);
elf->sec[ELF_SEC_TEXT].offset = shdr[i].offset;
ESP_LOGD(TAG, ".text offset is 0x%lx size is 0x%x",
elf->sec[ELF_SEC_TEXT].offset,
elf->sec[ELF_SEC_TEXT].size);
} else if (sflags(&shdr[i], SHF_WRITE) && !strcmp(ELF_DATA, name)) {
ESP_LOGD(TAG, ".data sec addr=0x%08x size=0x%08x offset=0x%08x",
shdr[i].addr, shdr[i].size, shdr[i].offset);
elf->sec[ELF_SEC_DATA].v_addr = shdr[i].addr;
elf->sec[ELF_SEC_DATA].size = shdr[i].size;
elf->sec[ELF_SEC_DATA].offset = shdr[i].offset;
ESP_LOGD(TAG, ".data offset is 0x%lx size is 0x%x",
elf->sec[ELF_SEC_DATA].offset,
elf->sec[ELF_SEC_DATA].size);
} else if (!strcmp(ELF_RODATA, name)) {
ESP_LOGD(TAG, ".rodata sec addr=0x%08x size=0x%08x offset=0x%08x",
shdr[i].addr, shdr[i].size, shdr[i].offset);
elf->sec[ELF_SEC_RODATA].v_addr = shdr[i].addr;
elf->sec[ELF_SEC_RODATA].size = shdr[i].size;
elf->sec[ELF_SEC_RODATA].offset = shdr[i].offset;
ESP_LOGD(TAG, ".rodata offset is 0x%lx size is 0x%x",
elf->sec[ELF_SEC_RODATA].offset,
elf->sec[ELF_SEC_RODATA].size);
} else if (!strcmp(ELF_DATA_REL_RO, name)) {
ESP_LOGD(TAG, ".data.rel.ro sec addr=0x%08x size=0x%08x offset=0x%08x",
shdr[i].addr, shdr[i].size, shdr[i].offset);
elf->sec[ELF_SEC_DRLRO].v_addr = shdr[i].addr;
elf->sec[ELF_SEC_DRLRO].size = shdr[i].size;
elf->sec[ELF_SEC_DRLRO].offset = shdr[i].offset;
ESP_LOGD(TAG, ".data.rel.ro offset is 0x%lx size is 0x%x",
elf->sec[ELF_SEC_DRLRO].offset,
elf->sec[ELF_SEC_DRLRO].size);
}
} else if (stype(&shdr[i], SHT_NOBITS) &&
sflags(&shdr[i], SHF_ALLOC | SHF_WRITE) &&
!strcmp(ELF_BSS, name)) {
ESP_LOGD(TAG, ".bss sec addr=0x%08x size=0x%08x offset=0x%08x",
shdr[i].addr, shdr[i].size, shdr[i].offset);
elf->sec[ELF_SEC_BSS].v_addr = shdr[i].addr;
elf->sec[ELF_SEC_BSS].size = shdr[i].size;
elf->sec[ELF_SEC_BSS].offset = shdr[i].offset;
ESP_LOGD(TAG, ".bss offset is 0x%lx size is 0x%x",
elf->sec[ELF_SEC_BSS].offset,
elf->sec[ELF_SEC_BSS].size);
}
}
/* No .text on image */
if (!elf->sec[ELF_SEC_TEXT].size) {
return -EINVAL;
}
elf->ptext = esp_elf_malloc(elf->sec[ELF_SEC_TEXT].size, true);
if (!elf->ptext) {
return -ENOMEM;
}
size = elf->sec[ELF_SEC_DATA].size +
elf->sec[ELF_SEC_RODATA].size +
elf->sec[ELF_SEC_BSS].size +
elf->sec[ELF_SEC_DRLRO].size;
if (size) {
elf->pdata = esp_elf_malloc(size, false);
if (!elf->pdata) {
esp_elf_free(elf->ptext);
return -ENOMEM;
}
}
/* Dump ".text" from ELF to executable space memory */
elf->sec[ELF_SEC_TEXT].addr = (Elf32_Addr)elf->ptext;
memcpy(elf->ptext, pbuf + elf->sec[ELF_SEC_TEXT].offset,
elf->sec[ELF_SEC_TEXT].size);
#ifdef CONFIG_ELF_LOADER_SET_MMU
if (esp_elf_arch_init_mmu(elf)) {
esp_elf_free(elf->ptext);
esp_elf_free(elf->pdata);
return -EIO;
}
#endif
/**
* Dump ".data", ".rodata" and ".bss" from ELF to R/W space memory.
*
* Todo: Dump ".rodata" to rodata section by MMU/MPU.
*/
if (size) {
uint8_t *pdata = elf->pdata;
if (elf->sec[ELF_SEC_DATA].size) {
elf->sec[ELF_SEC_DATA].addr = (uint32_t)pdata;
memcpy(pdata, pbuf + elf->sec[ELF_SEC_DATA].offset,
elf->sec[ELF_SEC_DATA].size);
pdata += elf->sec[ELF_SEC_DATA].size;
}
if (elf->sec[ELF_SEC_RODATA].size) {
elf->sec[ELF_SEC_RODATA].addr = (uint32_t)pdata;
memcpy(pdata, pbuf + elf->sec[ELF_SEC_RODATA].offset,
elf->sec[ELF_SEC_RODATA].size);
pdata += elf->sec[ELF_SEC_RODATA].size;
}
if (elf->sec[ELF_SEC_DRLRO].size) {
elf->sec[ELF_SEC_DRLRO].addr = (uint32_t)pdata;
memcpy(pdata, pbuf + elf->sec[ELF_SEC_DRLRO].offset,
elf->sec[ELF_SEC_DRLRO].size);
pdata += elf->sec[ELF_SEC_DRLRO].size;
}
if (elf->sec[ELF_SEC_BSS].size) {
elf->sec[ELF_SEC_BSS].addr = (uint32_t)pdata;
memset(pdata, 0, elf->sec[ELF_SEC_BSS].size);
}
}
/* Set ELF entry */
entry = ehdr->entry + elf->sec[ELF_SEC_TEXT].addr -
elf->sec[ELF_SEC_TEXT].v_addr;
#ifdef CONFIG_ELF_LOADER_CACHE_OFFSET
elf->entry = (void *)elf_remap_text(elf, (uintptr_t)entry);
#else
elf->entry = (void *)entry;
#endif
return 0;
}
#else
/**
* @brief Load ELF segment.
*
* @param elf - ELF object pointer
* @param pbuf - ELF data buffer
*
* @return ESP_OK if success or other if failed.
*/
static int esp_elf_load_segment(esp_elf_t *elf, const uint8_t *pbuf)
{
uint32_t size;
bool first_segment = false;
Elf32_Addr vaddr_s = 0;
Elf32_Addr vaddr_e = 0;
const elf32_hdr_t *ehdr = (const elf32_hdr_t *)pbuf;
const elf32_phdr_t *phdr = (const elf32_phdr_t *)(pbuf + ehdr->phoff);
for (int i = 0; i < ehdr->phnum; i++) {
if (phdr[i].type != PT_LOAD) {
continue;
}
if (phdr[i].memsz < phdr[i].filesz) {
ESP_LOGE(TAG, "Invalid segment[%d], memsz: %d, filesz: %d",
i, phdr[i].memsz, phdr[i].filesz);
return -EINVAL;
}
if (first_segment == true) {
vaddr_s = phdr[i].vaddr;
vaddr_e = phdr[i].vaddr + phdr[i].memsz;
first_segment = true;
if (vaddr_e < vaddr_s) {
ESP_LOGE(TAG, "Invalid segment[%d], vaddr: 0x%x, memsz: %d",
i, phdr[i].vaddr, phdr[i].memsz);
return -EINVAL;
}
} else {
if (phdr[i].vaddr < vaddr_e) {
ESP_LOGE(TAG, "Invalid segment[%d], should not overlap, vaddr: 0x%x, vaddr_e: 0x%x\n",
i, phdr[i].vaddr, vaddr_e);
return -EINVAL;
}
if (phdr[i].vaddr > vaddr_e + ADDR_OFFSET) {
ESP_LOGI(TAG, "Too much padding before segment[%d], padding: %d",
i, phdr[i].vaddr - vaddr_e);
}
vaddr_e = phdr[i].vaddr + phdr[i].memsz;
if (vaddr_e < phdr[i].vaddr) {
ESP_LOGE(TAG, "Invalid segment[%d], address overflow, vaddr: 0x%x, vaddr_e: 0x%x\n",
i, phdr[i].vaddr, vaddr_e);
return -EINVAL;
}
}
ESP_LOGD(TAG, "LOAD segment[%d], vaddr: 0x%x, memsize: 0x%08x",
i, phdr[i].vaddr, phdr[i].memsz);
}
size = vaddr_e - vaddr_s;
if (size == 0) {
return -EINVAL;
}
elf->svaddr = vaddr_s;
elf->psegment = esp_elf_malloc(size, true);
if (!elf->psegment) {
return -ENOMEM;
}
memset(elf->psegment, 0, size);
/* Dump "PT_LOAD" from ELF to memory space */
for (int i = 0; i < ehdr->phnum; i++) {
if (phdr[i].type == PT_LOAD) {
memcpy(elf->psegment + phdr[i].vaddr - vaddr_s,
(uint8_t *)pbuf + phdr[i].offset, phdr[i].filesz);
ESP_LOGD(TAG, "Copy segment[%d], mem_addr: 0x%x, vaddr: 0x%x, size: 0x%08x",
i, (int)((uint8_t *)elf->psegment + phdr[i].vaddr - vaddr_s),
phdr[i].vaddr, phdr[i].filesz);
}
}
#if SOC_CACHE_INTERNAL_MEM_VIA_L1CACHE
cache_ll_writeback_all(CACHE_LL_LEVEL_INT_MEM, CACHE_TYPE_DATA, CACHE_LL_ID_ALL);
#endif
elf->entry = (void *)((uint8_t *)elf->psegment + ehdr->entry - vaddr_s);
return 0;
}
#endif
/**
* @brief Override the internal symbol resolver.
* The default resolver is based on static lists that are determined by KConfig.
* This override allows for an arbitrary implementation.
*
* @param resolver the resolver function
*/
void elf_set_symbol_resolver(symbol_resolver resolver) {
current_resolver = resolver;
}
/**
* @brief Map symbol's address of ELF to physic space.
*
* @param elf - ELF object pointer
* @param sym - ELF symbol address
*
* @return ESP_OK if success or other if failed.
*/
uintptr_t esp_elf_map_sym(esp_elf_t *elf, uintptr_t sym)
{
for (int i = 0; i < ELF_SECS; i++) {
if ((sym >= elf->sec[i].v_addr) &&
(sym < (elf->sec[i].v_addr + elf->sec[i].size))) {
return sym - elf->sec[i].v_addr + elf->sec[i].addr;
}
}
return 0;
}
/**
* @brief Initialize ELF object.
*
* @param elf - ELF object pointer
*
* @return ESP_OK if success or other if failed.
*/
int esp_elf_init(esp_elf_t *elf)
{
ESP_LOGI(TAG, "ELF loader version: %d.%d.%d", ELF_LOADER_VER_MAJOR, ELF_LOADER_VER_MINOR, ELF_LOADER_VER_PATCH);
if (!elf) {
return -EINVAL;
}
memset(elf, 0, sizeof(esp_elf_t));
return 0;
}
/**
* @brief Decode and relocate ELF data.
*
* @param elf - ELF object pointer
* @param pbuf - ELF data buffer
*
* @return ESP_OK if success or other if failed.
*/
int esp_elf_relocate(esp_elf_t *elf, const uint8_t *pbuf)
{
int ret;
const elf32_hdr_t *ehdr;
const elf32_shdr_t *shdr;
const char *shstrab;
if (!elf || !pbuf) {
return -EINVAL;
}
ehdr = (const elf32_hdr_t *)pbuf;
shdr = (const elf32_shdr_t *)(pbuf + ehdr->shoff);
shstrab = (const char *)pbuf + shdr[ehdr->shstrndx].offset;
/* Load section or segment to memory space */
#if CONFIG_ELF_LOADER_BUS_ADDRESS_MIRROR
ret = esp_elf_load_section(elf, pbuf);
#else
ret = esp_elf_load_segment(elf, pbuf);
#endif
if (ret) {
ESP_LOGE(TAG, "Error to load elf file, ret=%d", ret);
return ret;
}
ESP_LOGI(TAG, "elf->entry=%p\n", elf->entry);
/* Relocation section data */
for (uint32_t i = 0; i < ehdr->shnum; i++) {
if (stype(&shdr[i], SHT_RELA)) {
uint32_t nr_reloc;
const elf32_rela_t *rela;
const elf32_sym_t *symtab;
const char *strtab;
nr_reloc = shdr[i].size / sizeof(elf32_rela_t);
rela = (const elf32_rela_t *)(pbuf + shdr[i].offset);
symtab = (const elf32_sym_t *)(pbuf + shdr[shdr[i].link].offset);
strtab = (const char *)(pbuf + shdr[shdr[shdr[i].link].link].offset);
ESP_LOGD(TAG, "Section %s has %d symbol tables", shstrab + shdr[i].name, (int)nr_reloc);
for (int i = 0; i < nr_reloc; i++) {
int type;
uintptr_t addr = 0;
elf32_rela_t rela_buf;
memcpy(&rela_buf, &rela[i], sizeof(elf32_rela_t));
const elf32_sym_t *sym = &symtab[ELF_R_SYM(rela_buf.info)];
type = ELF_R_TYPE(rela_buf.info);
if (type == STT_COMMON || type == STT_OBJECT || type == STT_SECTION) {
const char *comm_name = strtab + sym->name;
if (comm_name[0]) {
addr = elf_find_sym(comm_name);
if (!addr) {
ESP_LOGE(TAG, "Can't find common %s", strtab + sym->name);
#if CONFIG_ELF_LOADER_BUS_ADDRESS_MIRROR
esp_elf_free(elf->pdata);
esp_elf_free(elf->ptext);
#else
esp_elf_free(elf->psegment);
#endif
return -ENOSYS;
}
ESP_LOGD(TAG, "Find common %s addr=%x", comm_name, addr);
}
} else if (type == STT_FILE) {
const char *func_name = strtab + sym->name;
if (sym->value) {
addr = esp_elf_map_sym(elf, sym->value);
} else {
addr = elf_find_sym(func_name);
}
if (!addr) {
ESP_LOGE(TAG, "Can't find symbol %s", func_name);
#if CONFIG_ELF_LOADER_BUS_ADDRESS_MIRROR
esp_elf_free(elf->pdata);
esp_elf_free(elf->ptext);
#else
esp_elf_free(elf->psegment);
#endif
return -ENOSYS;
}
ESP_LOGD(TAG, "Find function %s addr=%x", func_name, addr);
}
esp_elf_arch_relocate(elf, &rela_buf, sym, addr);
}
}
}
#ifdef CONFIG_ELF_LOADER_LOAD_PSRAM
esp_elf_arch_flush();
#endif
return 0;
}
/**
* @brief Request running relocated ELF function.
*
* @param elf - ELF object pointer
* @param opt - Request options
* @param argc - Arguments number
* @param argv - Arguments value array
*
* @return ESP_OK if success or other if failed.
*/
int esp_elf_request(esp_elf_t *elf, int opt, int argc, char *argv[])
{
if (!elf || !(elf->entry)) {
return -EINVAL;
}
elf->entry(argc, argv);
return 0;
}
/**
* @brief Deinitialize ELF object.
*
* @param elf - ELF object pointer
*
* @return None
*/
void esp_elf_deinit(esp_elf_t *elf)
{
#if CONFIG_ELF_LOADER_BUS_ADDRESS_MIRROR
if (elf->pdata) {
esp_elf_free(elf->pdata);
elf->pdata = NULL;
}
if (elf->ptext) {
esp_elf_free(elf->ptext);
elf->ptext = NULL;
}
#else
if (elf->psegment) {
esp_elf_free(elf->ptext);
elf->ptext = NULL;
}
#endif
#ifdef CONFIG_ELF_LOADER_SET_MMU
esp_elf_arch_deinit_mmu(elf);
#endif
}
/**
* @brief Print header description information of ELF.
*
* @param pbuf - ELF data buffer
*
* @return None
*/
void esp_elf_print_ehdr(const uint8_t *pbuf)
{
const char *s_bits, *s_endian;
const elf32_hdr_t *hdr = (const elf32_hdr_t *)pbuf;
switch (hdr->ident[4]) {
case 1:
s_bits = "32-bit";
break;
case 2:
s_bits = "64-bit";
break;
default:
s_bits = "invalid bits";
break;
}
switch (hdr->ident[5]) {
case 1:
s_endian = "little-endian";
break;
case 2:
s_endian = "big-endian";
break;
default:
s_endian = "invalid endian";
break;
}
if (hdr->ident[0] == 0x7f) {
ESP_LOGI(TAG, "%-40s %c%c%c", "Class:", hdr->ident[1], hdr->ident[2], hdr->ident[3]);
}
ESP_LOGI(TAG, "%-40s %s, %s", "Format:", s_bits, s_endian);
ESP_LOGI(TAG, "%-40s %x", "Version(current):", hdr->ident[6]);
ESP_LOGI(TAG, "%-40s %d", "Type:", hdr->type);
ESP_LOGI(TAG, "%-40s %d", "Machine:", hdr->machine);
ESP_LOGI(TAG, "%-40s %x", "Version:", hdr->version);
ESP_LOGI(TAG, "%-40s %x", "Entry point address:", hdr->entry);
ESP_LOGI(TAG, "%-40s %x", "Start of program headers:", hdr->phoff);
ESP_LOGI(TAG, "%-40s %d", "Start of section headers:", hdr->shoff);
ESP_LOGI(TAG, "%-40s 0x%x", "Flags:", hdr->flags);
ESP_LOGI(TAG, "%-40s %d", "Size of this header(bytes):", hdr->ehsize);
ESP_LOGI(TAG, "%-40s %d", "Size of program headers(bytes):", hdr->phentsize);
ESP_LOGI(TAG, "%-40s %d", "Number of program headers:", hdr->phnum);
ESP_LOGI(TAG, "%-40s %d", "Size of section headers(bytes):", hdr->shentsize);
ESP_LOGI(TAG, "%-40s %d", "Number of section headers:", hdr->shnum);
ESP_LOGI(TAG, "%-40s %d", "Section header string table i:", hdr->shstrndx);
}
/**
* @brief Print program header description information of ELF.
*
* @param pbuf - ELF data buffer
*
* @return None
*/
void esp_elf_print_phdr(const uint8_t *pbuf)
{
const elf32_hdr_t *ehdr = (const elf32_hdr_t *)pbuf;
const elf32_phdr_t *phdr = (const elf32_phdr_t *)((size_t)pbuf + ehdr->phoff);
for (int i = 0; i < ehdr->phnum; i++) {
ESP_LOGI(TAG, "%-40s %d", "type:", phdr->type);
ESP_LOGI(TAG, "%-40s 0x%x", "offset:", phdr->offset);
ESP_LOGI(TAG, "%-40s 0x%x", "vaddr", phdr->vaddr);
ESP_LOGI(TAG, "%-40s 0x%x", "paddr:", phdr->paddr);
ESP_LOGI(TAG, "%-40s %d", "filesz", phdr->filesz);
ESP_LOGI(TAG, "%-40s %d", "memsz", phdr->memsz);
ESP_LOGI(TAG, "%-40s %d", "flags", phdr->flags);
ESP_LOGI(TAG, "%-40s 0x%x", "align", phdr->align);
phdr = (const elf32_phdr_t *)((size_t)phdr + sizeof(elf32_phdr_t));
}
}
/**
* @brief Print section header description information of ELF.
*
* @param pbuf - ELF data buffer
*
* @return None
*/
void esp_elf_print_shdr(const uint8_t *pbuf)
{
const elf32_hdr_t *ehdr = (const elf32_hdr_t *)pbuf;
const elf32_shdr_t *shdr = (const elf32_shdr_t *)((size_t)pbuf + ehdr->shoff);
for (int i = 0; i < ehdr->shnum; i++) {
ESP_LOGI(TAG, "%-40s %d", "name:", shdr->name);
ESP_LOGI(TAG, "%-40s %d", "type:", shdr->type);
ESP_LOGI(TAG, "%-40s 0x%x", "flags:", shdr->flags);
ESP_LOGI(TAG, "%-40s %x", "addr", shdr->addr);
ESP_LOGI(TAG, "%-40s %x", "offset:", shdr->offset);
ESP_LOGI(TAG, "%-40s %d", "size", shdr->size);
ESP_LOGI(TAG, "%-40s 0x%x", "link", shdr->link);
ESP_LOGI(TAG, "%-40s %d", "addralign", shdr->addralign);
ESP_LOGI(TAG, "%-40s %d", "entsize", shdr->entsize);
shdr = (const elf32_shdr_t *)((size_t)shdr + sizeof(elf32_shdr_t));
}
}
/**
* @brief Print section information of ELF.
*
* @param pbuf - ELF data buffer
*
* @return None
*/
void esp_elf_print_sec(esp_elf_t *elf)
{
const char *sec_names[ELF_SECS] = {
"text", "bss", "data", "rodata"
};
for (int i = 0; i < ELF_SECS; i++) {
ESP_LOGI(TAG, "%s: 0x%08x size 0x%08x", sec_names[i], elf->sec[i].addr, elf->sec[i].size);
}
ESP_LOGI(TAG, "entry: %p", elf->entry);
}
-120
View File
@@ -1,120 +0,0 @@
/*
* SPDX-FileCopyrightText: 2023 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#include <assert.h>
#include <sys/errno.h>
#include "esp_idf_version.h"
#include "esp_attr.h"
#include "esp_heap_caps.h"
#include "soc/soc.h"
#include "private/elf_platform.h"
#ifdef CONFIG_ELF_LOADER_LOAD_PSRAM
#ifdef CONFIG_IDF_TARGET_ESP32S3
#define OFFSET_TEXT_VALUE (SOC_IROM_LOW - SOC_DROM_LOW)
#endif
#endif
/**
* @brief Allocate block of memory.
*
* @param n - Memory size in byte
* @param exec - True: memory can run executable code; False: memory can R/W data
*
* @return Memory pointer if success or NULL if failed.
*/
void *esp_elf_malloc(uint32_t n, bool exec)
{
uint32_t caps;
#if CONFIG_ELF_LOADER_BUS_ADDRESS_MIRROR
#ifdef CONFIG_ELF_LOADER_LOAD_PSRAM
caps = MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT;
#else
caps = exec ? MALLOC_CAP_EXEC : MALLOC_CAP_8BIT;
#endif
#else
#ifdef CONFIG_ELF_LOADER_LOAD_PSRAM
caps = MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT;
#else
caps = MALLOC_CAP_8BIT;
#endif
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 3, 0)
caps |= MALLOC_CAP_CACHE_ALIGNED;
#endif
#endif
return heap_caps_malloc(n, caps);
}
/**
* @brief Free block of memory.
*
* @param ptr - memory block pointer allocated by "esp_elf_malloc"
*
* @return None
*/
void esp_elf_free(void *ptr)
{
heap_caps_free(ptr);
}
/**
* @brief Remap symbol from ".data" to ".text" section.
*
* @param elf - ELF object pointer
* @param sym - ELF symbol table
*
* @return Remapped symbol value
*/
#ifdef CONFIG_ELF_LOADER_CACHE_OFFSET
uintptr_t elf_remap_text(esp_elf_t *elf, uintptr_t sym)
{
uintptr_t mapped_sym;
esp_elf_sec_t *sec = &elf->sec[ELF_SEC_TEXT];
if ((sym >= sec->addr) &&
(sym < (sec->addr + sec->size))) {
#ifdef CONFIG_ELF_LOADER_SET_MMU
mapped_sym = sym + elf->text_off;
#else
mapped_sym = sym + OFFSET_TEXT_VALUE;
#endif
} else {
mapped_sym = sym;
}
return mapped_sym;
}
#endif
/**
* @brief Flush data from cache to external RAM.
*
* @param None
*
* @return None
*/
#ifdef CONFIG_ELF_LOADER_LOAD_PSRAM
void IRAM_ATTR esp_elf_arch_flush(void)
{
extern void spi_flash_disable_interrupts_caches_and_other_cpu(void);
extern void spi_flash_enable_interrupts_caches_and_other_cpu(void);
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 0, 0)
extern void Cache_WriteBack_All(void);
Cache_WriteBack_All();
#else
void esp_spiram_writeback_cache(void);
esp_spiram_writeback_cache();
#endif
spi_flash_disable_interrupts_caches_and_other_cpu();
spi_flash_enable_interrupts_caches_and_other_cpu();
}
#endif
-205
View File
@@ -1,205 +0,0 @@
/*
* SPDX-FileCopyrightText: 2023 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <time.h>
#include <reent.h>
#include <pthread.h>
#include <setjmp.h>
#include <getopt.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <ctype.h>
#include "rom/ets_sys.h"
#include "private/elf_symbol.h"
extern int __ltdf2(double a, double b);
extern unsigned int __fixunsdfsi(double a);
extern int __gtdf2(double a, double b);
extern double __floatunsidf(unsigned int i);
extern double __divdf3(double a, double b);
/** @brief Libc public functions symbols look-up table */
static const struct esp_elfsym g_esp_libc_elfsyms[] = {
/* string.h */
ESP_ELFSYM_EXPORT(strerror),
ESP_ELFSYM_EXPORT(memset),
ESP_ELFSYM_EXPORT(memcpy),
ESP_ELFSYM_EXPORT(strlen),
ESP_ELFSYM_EXPORT(strtod),
ESP_ELFSYM_EXPORT(strrchr),
ESP_ELFSYM_EXPORT(strchr),
ESP_ELFSYM_EXPORT(strcmp),
ESP_ELFSYM_EXPORT(strtol),
ESP_ELFSYM_EXPORT(strcspn),
ESP_ELFSYM_EXPORT(strncat),
/* stdio.h */
ESP_ELFSYM_EXPORT(puts),
ESP_ELFSYM_EXPORT(putchar),
ESP_ELFSYM_EXPORT(fputc),
ESP_ELFSYM_EXPORT(fputs),
ESP_ELFSYM_EXPORT(printf),
ESP_ELFSYM_EXPORT(vfprintf),
ESP_ELFSYM_EXPORT(fprintf),
ESP_ELFSYM_EXPORT(fwrite),
/* unistd.h */
ESP_ELFSYM_EXPORT(usleep),
ESP_ELFSYM_EXPORT(sleep),
ESP_ELFSYM_EXPORT(exit),
ESP_ELFSYM_EXPORT(close),
/* stdlib.h */
ESP_ELFSYM_EXPORT(malloc),
ESP_ELFSYM_EXPORT(calloc),
ESP_ELFSYM_EXPORT(realloc),
ESP_ELFSYM_EXPORT(free),
/* time.h */
ESP_ELFSYM_EXPORT(clock_gettime),
ESP_ELFSYM_EXPORT(strftime),
/* pthread.h */
ESP_ELFSYM_EXPORT(pthread_create),
ESP_ELFSYM_EXPORT(pthread_attr_init),
ESP_ELFSYM_EXPORT(pthread_attr_setstacksize),
ESP_ELFSYM_EXPORT(pthread_detach),
ESP_ELFSYM_EXPORT(pthread_join),
ESP_ELFSYM_EXPORT(pthread_exit),
/* newlib */
ESP_ELFSYM_EXPORT(__errno),
ESP_ELFSYM_EXPORT(__getreent),
#ifdef __HAVE_LOCALE_INFO__
ESP_ELFSYM_EXPORT(__locale_ctype_ptr),
#else
ESP_ELFSYM_EXPORT(_ctype_),
#endif
/* math */
ESP_ELFSYM_EXPORT(__ltdf2),
ESP_ELFSYM_EXPORT(__fixunsdfsi),
ESP_ELFSYM_EXPORT(__gtdf2),
ESP_ELFSYM_EXPORT(__floatunsidf),
ESP_ELFSYM_EXPORT(__divdf3),
/* getopt.h */
ESP_ELFSYM_EXPORT(getopt_long),
ESP_ELFSYM_EXPORT(optind),
ESP_ELFSYM_EXPORT(opterr),
ESP_ELFSYM_EXPORT(optarg),
ESP_ELFSYM_EXPORT(optopt),
/* setjmp.h */
ESP_ELFSYM_EXPORT(longjmp),
ESP_ELFSYM_EXPORT(setjmp),
ESP_ELFSYM_END
};
/** @brief ESP-IDF public functions symbols look-up table */
static const struct esp_elfsym g_esp_espidf_elfsyms[] = {
/* sys/socket.h */
ESP_ELFSYM_EXPORT(lwip_bind),
ESP_ELFSYM_EXPORT(lwip_setsockopt),
ESP_ELFSYM_EXPORT(lwip_socket),
ESP_ELFSYM_EXPORT(lwip_listen),
ESP_ELFSYM_EXPORT(lwip_accept),
ESP_ELFSYM_EXPORT(lwip_recv),
ESP_ELFSYM_EXPORT(lwip_recvfrom),
ESP_ELFSYM_EXPORT(lwip_send),
ESP_ELFSYM_EXPORT(lwip_sendto),
ESP_ELFSYM_EXPORT(lwip_connect),
/* arpa/inet.h */
ESP_ELFSYM_EXPORT(ipaddr_addr),
ESP_ELFSYM_EXPORT(lwip_htons),
ESP_ELFSYM_EXPORT(lwip_htonl),
ESP_ELFSYM_EXPORT(ip4addr_ntoa),
/* ROM functions */
ESP_ELFSYM_EXPORT(ets_printf),
ESP_ELFSYM_END
};
/**
* @brief Find symbol address by name.
*
* @param sym_name - Symbol name
*
* @return Symbol address if success or 0 if failed.
*/
uintptr_t elf_find_sym_default(const char *sym_name)
{
const struct esp_elfsym *syms;
#ifdef CONFIG_ELF_LOADER_LIBC_SYMBOLS
syms = g_esp_libc_elfsyms;
while (syms->name) {
if (!strcmp(syms->name, sym_name)) {
return (uintptr_t)syms->sym;
}
syms++;
}
#else
syms = g_esp_libc_elfsyms;
(void)syms;
#endif
#ifdef CONFIG_ELF_LOADER_ESPIDF_SYMBOLS
syms = g_esp_espidf_elfsyms;
while (syms->name) {
if (!strcmp(syms->name, sym_name)) {
return (uintptr_t)syms->sym;
}
syms++;
}
#else
syms = g_esp_espidf_elfsyms;
(void)syms;
#endif
#ifdef CONFIG_ELF_LOADER_CUSTOMER_SYMBOLS
extern const struct esp_elfsym g_customer_elfsyms[];
syms = g_customer_elfsyms;
while (syms->name) {
if (!strcmp(syms->name, sym_name)) {
return (uintptr_t)syms->sym;
}
syms++;
}
#endif
return 0;
}
@@ -1,133 +0,0 @@
/*
* SPDX-FileCopyrightText: 2023 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#include <assert.h>
#include <sys/errno.h>
#include "esp_idf_version.h"
#include "esp_attr.h"
#include "esp_heap_caps.h"
#include "esp32s2/rom/cache.h"
#if ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 0, 0)
#include "esp32s2/spiram.h"
#endif
#include "soc/mmu.h"
#include "private/elf_platform.h"
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 0, 0)
#define PSRAM_VADDR_START 0x3f800000
#else
#define PSRAM_VADDR_START (DRAM0_CACHE_ADDRESS_HIGH - esp_spiram_get_size())
#endif
#define MMU_INVALID BIT(14)
#define MMU_UNIT_SIZE 0x10000
#define MMU_REG ((volatile uint32_t *)DR_REG_MMU_TABLE)
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 2, 0)
#define IRAM0_ADDRESS_LOW SOC_IRAM0_ADDRESS_LOW
#define IRAM0_ADDRESS_HIGH SOC_IRAM0_ADDRESS_HIGH
#define MMU_ACCESS_SPIRAM SOC_MMU_ACCESS_SPIRAM
#endif
#define ESP_MMU_IBUS_BASE IRAM0_ADDRESS_LOW
#define ESP_MMU_IBUS_MAX ((IRAM0_ADDRESS_HIGH - IRAM0_ADDRESS_LOW) / \
MMU_UNIT_SIZE)
#define ESP_MMU_IBUS_OFF (8)
#define DADDR_2_SECS(v) ((v) / MMU_UNIT_SIZE)
#define IADDR_2_SECS(v) ((v) / MMU_UNIT_SIZE)
#define PSRAM_OFF(v) ((v) - PSRAM_VADDR_START)
#define PSRAM_SECS(v) DADDR_2_SECS(PSRAM_OFF((uintptr_t)(v)))
#define PSRAM_ALIGN(v) ((uintptr_t)(v) & (~(MMU_UNIT_SIZE - 1)))
#define ICACHE_ADDR(s) (ESP_MMU_IBUS_BASE + (s) * MMU_UNIT_SIZE)
extern void spi_flash_disable_interrupts_caches_and_other_cpu(void);
extern void spi_flash_enable_interrupts_caches_and_other_cpu(void);
/**
* @brief Initialize MMU hardware remapping function.
*
* @param elf - ELF object pointer
*
* @return 0 if success or a negative value if failed.
*/
int IRAM_ATTR esp_elf_arch_init_mmu(esp_elf_t *elf)
{
int ret;
int off;
uint32_t ibus_secs;
volatile uint32_t dbus_secs;
esp_elf_sec_t *sec = &elf->sec[ELF_SEC_TEXT];
volatile uint32_t *p = MMU_REG;
if (sec->size % MMU_UNIT_SIZE) {
ibus_secs = IADDR_2_SECS(sec->size) + 1;
} else {
ibus_secs = IADDR_2_SECS(sec->size);
}
dbus_secs = PSRAM_SECS(elf->ptext);
spi_flash_disable_interrupts_caches_and_other_cpu();
for (off = ESP_MMU_IBUS_OFF; off < ESP_MMU_IBUS_MAX; off++) {
if (p[off] == MMU_INVALID) {
int j;
for (j = 1; j < ibus_secs; j++) {
if (p[off + j] != MMU_INVALID) {
break;
}
}
if (j >= ibus_secs) {
for (int k = 0; k < ibus_secs; k++) {
p [off + k] = MMU_ACCESS_SPIRAM | (dbus_secs + k);
}
break;
}
}
}
spi_flash_enable_interrupts_caches_and_other_cpu();
if (off >= ESP_MMU_IBUS_MAX) {
ret = -EIO;
} else {
elf->mmu_off = off;
elf->mmu_num = ibus_secs;
elf->text_off = ICACHE_ADDR(off) - PSRAM_ALIGN(elf->ptext);
ret = 0;
}
return ret;
}
/**
* @brief De-initialize MMU hardware remapping function.
*
* @param elf - ELF object pointer
*
* @return None
*/
void IRAM_ATTR esp_elf_arch_deinit_mmu(esp_elf_t *elf)
{
volatile int num = elf->mmu_num;
volatile int off = elf->mmu_off;
volatile uint32_t *p = MMU_REG;
spi_flash_disable_interrupts_caches_and_other_cpu();
for (int i = 0; i < num; i++) {
p [off + i] = MMU_INVALID;
}
spi_flash_enable_interrupts_caches_and_other_cpu();
}
-268
View File
@@ -1,268 +0,0 @@
#!/usr/bin/env python
#
# SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD
#
# SPDX-License-Identifier: Apache-2.0
import logging
import os
import argparse
import sys
import re
import subprocess
def get_global_symbols(lines, type, undefined=False, symbol_types=None):
"""
Extract global symbols from the given lines of a symbol table.
:param lines: List of lines from the symbol table, each representing a symbol.
:param type: Type of the input file ('e' for ELF files, 'l' for static libraries).
:param undefined: If True, only extract undefined (UND) symbols; otherwise, extract all GLOBAL symbols.
:param symbol_types: A list of symbol types to filter by (e.g., ['FUNC', 'OBJECT']). If None, no filtering by type.
:return: List of symbol names if any match the criteria; otherwise, returns an empty list.
"""
symbols = []
if type == 'e':
# Pattern for ELF files
if not undefined:
pattern = re.compile(
r'^\s*\d+:\s+(\S+)\s+(\d+)\s+(FUNC|OBJECT)\s+GLOBAL\s+DEFAULT\s+(?:\d+|ABS|UND|COM|DEBUG)\s+(\S+)',
re.MULTILINE
)
else:
pattern = re.compile(
r'^\s*\d*:\s*\w*\s*\d*\s*NOTYPE\s*GLOBAL\s*DEFAULT\s*UND\s+(\S*)',
re.MULTILINE
)
for line in lines:
match = pattern.match(line)
if match:
if not undefined:
address, size, symbol_type, symbol_name = match.groups()
# Filter by symbol type if specified
if symbol_types and symbol_type not in symbol_types:
continue
symbols.append(symbol_name)
else:
symbol_name = match.group(1)
symbols.append(symbol_name)
elif type == 'l':
# Patterns for static libraries
func_pattern = re.compile(r'^\s*[0-9a-fA-F]+\s+[TD]\s+(\S+)$')
var_pattern = re.compile(r'^\s*[0-9a-fA-F]+\s+[BD]\s+(\S+)$')
for line in lines:
if not undefined:
func_match = func_pattern.match(line)
var_match = var_pattern.match(line)
if func_match:
symbols.append(func_match.group(1))
elif var_match:
symbols.append(var_match.group(1))
return symbols
def save_c_file(symbols, output, symbol_table, exclude_symbols=None):
"""
Write extern declarations and ESP_ELFSYM structure to a C file, excluding specified symbols.
:param symbols: List of symbol names.
:param output: Path to the output C file.
:param exclude_symbols: List of symbol names to exclude; defaults to ['elf_find_sym'].
"""
if exclude_symbols is None:
exclude_symbols = ['elf_find_sym'] # Set default excluded symbols
# Filter out excluded symbols
filtered_symbols = [name for name in symbols if name not in exclude_symbols]
# Build the content of the C file
buf = '/*\n'
buf += ' * SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD\n'
buf += ' *\n'
buf += ' * SPDX-License-Identifier: Apache-2.0\n'
buf += ' */\n\n'
# Add standard library headers
libc_headers = ['stddef'] # Add more headers as needed
buf += '\n'.join([f'#include <{h}.h>' for h in libc_headers]) + '\n\n'
# Add custom header
buf += '#include "private/elf_symbol.h"\n\n'
# Generate extern declarations if there are symbols
if filtered_symbols:
buf += '/* Extern declarations from ELF symbol table */\n\n'
buf += '#pragma GCC diagnostic push\n'
buf += '#pragma GCC diagnostic ignored "-Wbuiltin-declaration-mismatch"\n'
for symbol_name in filtered_symbols:
buf += f'extern int {symbol_name};\n'
buf += '#pragma GCC diagnostic pop\n\n'
# Define the symbol table structure with dynamic variable name
symbol_table_var = f'g_{symbol_table}_elfsyms'
buf += f'/* Available ELF symbols table: {symbol_table_var} */\n'
buf += f'\nconst struct esp_elfsym {symbol_table_var}[] = {{\n'
# Generate ESP_ELFSYM_EXPORT entries
if filtered_symbols:
for symbol_name in filtered_symbols:
buf += f' ESP_ELFSYM_EXPORT({symbol_name}),\n'
# End the symbol table
buf += ' ESP_ELFSYM_END\n'
buf += '};\n'
# Write to the file
with open(output, 'w+') as f:
f.write(buf)
def main():
"""
Main function to parse command-line arguments and process the input file's symbol table.
"""
parser = argparse.ArgumentParser(description='Extract all public functions from an application project', prog='symbols')
parser.add_argument(
'--output-file', '-of',
help='Custom output file path with filename (overrides --output)',
default=None
)
parser.add_argument(
'--input', '-i',
help='Input file name with full path',
required=True
)
parser.add_argument(
'--undefined', '-u',
action='store_true',
help='If set, only extract undefined (UND) symbols; otherwise, extract all GLOBAL symbols.',
default=False
)
parser.add_argument(
'--exclude', '-e',
nargs='+',
help='Symbols to exclude from the generated C file (e.g., memcpy __ltdf2). Default: elf_find_sym',
default=[] # User can extend this list
)
parser.add_argument(
'--type', '-t',
choices=['e', 'l'],
required=True,
help='Type of the input file: "elf" for ELF file, "lib" for static library (.a)'
)
parser.add_argument(
'--debug', '-d',
help='Debug level(option is \'debug\')',
default='no',
type=str)
args = parser.parse_args()
# Configure logging
if args.debug == 'debug':
logging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(levelname)s - %(message)s')
else:
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
# Get the absolute path of the current file
cur_dir = os.path.dirname(os.path.abspath(__file__))
if args.type == 'e':
extracted_part = 'customer'
elif args.type == 'l':
# Convert relative path to absolute path
input_abs_path = os.path.abspath(args.input)
# Get the base name of the input file (without extension)
input_basename = os.path.basename(input_abs_path)
# Use a regular expression to extract the middle part
match = re.search(r'^lib(.+)\.a$', input_basename)
if match:
extracted_part = match.group(1)
else:
logging.error('Invalid input file name format. Expected format: lib<name>.a')
sys.exit(1)
# Determine the output file path
if args.output_file:
# Use the custom file path provided by the user
elfsym_file_dir = os.path.abspath(args.output_file)
output_dir = os.path.dirname(elfsym_file_dir)
output_abs_path = os.path.abspath(args.output_file)
output_basename = os.path.basename(output_abs_path)
extracted_part = os.path.splitext(output_basename)[0]
else:
# Use the default behavior: generate the file in the parent directory's 'src' folder
parent_dir = os.path.dirname(cur_dir)
output_dir = os.path.join(parent_dir, 'src') # Default directory is 'src' under the parent directory
output_file_name = f'esp_all_symbol.c'
elfsym_file_dir = os.path.join(output_dir, output_file_name)
# Ensure the output directory exists
os.makedirs(output_dir, exist_ok=True)
# Set default excluded symbols and allow user to extend the list
exclude_symbols = ['elf_find_sym', 'g_customer_elfsyms'] + args.exclude
if args.type == 'e':
cmd = ['readelf', '-s', '-W', args.input]
elif args.type == 'l':
cmd = ['nm', '--defined-only', '-g', args.input]
# Execute the readelf or nm command for static libraries
try:
result = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, check=True)
lines = result.stdout.splitlines()
except subprocess.CalledProcessError as e:
logging.error(f'Error executing command: {e.stderr}')
sys.exit(1)
except FileNotFoundError:
logging.error('nm command not found. Please ensure it is installed and available in your PATH.')
sys.exit(1)
# Extract global symbols from ELF file or static library
symbols = get_global_symbols(lines, type=args.type, undefined=args.undefined, symbol_types=['FUNC', 'OBJECT'])
if not symbols:
logging.warning('No global symbols found in the input file.')
sys.exit(0)
logging.debug('symbols: %s'%(cmd))
logging.debug('symbols: %s'%(symbols))
logging.debug('elfsym_file_dir: %s'%(elfsym_file_dir))
logging.debug('extracted_part: %s'%(extracted_part))
logging.debug('exclude_symbols: %s'%(exclude_symbols))
# Save the C file
try:
save_c_file(symbols, elfsym_file_dir, extracted_part, exclude_symbols=exclude_symbols)
logging.info(f"C file with extern declarations and symbol table has been saved to '{elfsym_file_dir}'.")
except Exception as e:
logging.error(f'Error writing C file: {e}')
sys.exit(1)
def _main():
"""
Wrapper for the main function to catch and handle runtime errors.
"""
try:
main()
except RuntimeError as e:
logging.error(f'A fatal error occurred: {e}')
sys.exit(2)
if __name__ == '__main__':
_main()
@@ -3,6 +3,7 @@
#include <sdkconfig.h>
#endif
#include <app/elf_check.h>
#include <app/loader.h>
#include <app/location.h>
@@ -15,6 +16,8 @@
#include <esp_elf.h>
#include <esp_err.h>
#include <sys/stat.h>
#include <cstdio>
#include <new>
#include <string>
@@ -63,13 +66,43 @@ error_t read_file(const char* path, uint8_t** out_data, size_t* out_size) {
return ERROR_NONE;
}
// location.location can be either an app's install directory or the .elf file directly; the
// former resolves to the per-target binary at {dir}/elf/{CONFIG_IDF_TARGET}.elf.
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 .elf file directly. A
// "packaged" app's install directory always holds its single binary at the fixed path
// {dir}/bin/{CONFIG_IDF_TARGET}/app.elf - a "terminal" app has no such file (its several
// binaries keep their own names), so this correctly leaves it unresolvable - terminal apps
// aren't run through AppLoaderApi (see app/install.h).
std::string resolve_elf_path(const std::string& path) {
if (path.ends_with(".elf")) {
return path;
}
return path + "/elf/" + CONFIG_IDF_TARGET + ".elf";
std::string candidate = path + "/bin/" CONFIG_IDF_TARGET "/app.elf";
return is_regular_file(candidate) ? candidate : "";
}
constexpr ElfRequirements EXECUTABLE_REQUIREMENTS = {
.elf_class = ELF_CLASS_32,
.data = ELF_DATA_2LSB,
.type = ELF_TYPE_DYN,
#if defined(__XTENSA__)
.machine = ELF_MACHINE_XTENSA,
#elif defined(__riscv)
.machine = ELF_MACHINE_RISCV,
#else
#error "Unsupported ESP32 architecture for ELF machine check"
#endif
};
// Validates an already-resolved binary path (see resolve_elf_path()) before it's handed to
// esp_elf_relocate(), which performs no header validation of its own: the extension check is a
// cheap string comparison, so the file is only opened as a last resort.
bool is_executable_file(const std::string& resolved_path) {
return resolved_path.ends_with(".elf")
&& elf_check_file(resolved_path.c_str(), &EXECUTABLE_REQUIREMENTS);
}
error_t api_load(AppLocation location, AppRuntime* out_runtime) {
@@ -88,6 +121,12 @@ error_t api_load(AppLocation location, AppRuntime* out_runtime) {
auto elf_path = resolve_elf_path(static_cast<const char*>(location.location));
if (!is_executable_file(elf_path)) {
LOG_E(TAG, "Not executable: %s", elf_path.c_str());
delete runtime;
return ERROR_NOT_ALLOWED;
}
size_t size = 0;
error_t read_result = read_file(elf_path.c_str(), &runtime->file_data, &size);
if (read_result != ERROR_NONE) {
@@ -127,10 +166,20 @@ void api_unload(AppRuntime runtime_ptr) {
delete runtime;
}
bool api_is_executable(AppLocation location) {
if (location.type != APP_LOCATION_PATH) {
return false;
}
auto elf_path = resolve_elf_path(static_cast<const char*>(location.location));
return is_executable_file(elf_path);
}
AppLoaderApi loader_api = {
.load = api_load,
.run = api_run,
.unload = api_unload,
.is_executable = api_is_executable,
};
void* create_service(const ServiceManifest*) {
+15 -1
View File
@@ -1,6 +1,8 @@
// SPDX-License-Identifier: Apache-2.0
#include <app_esp32/module.h>
#include <private/elf_symbol.h>
#include <service/manager.h>
#include <tactility/error.h>
@@ -10,11 +12,23 @@ extern "C" {
extern ServiceManifest loader_service_manifest;
// Overrides elf_loader's default KConfig-based symbol resolver with one that looks up symbols
// across every started kernel module's own symbol table instead.
uintptr_t app_esp32_symbol_resolver(const char* symbolName) {
uintptr_t symbol_address;
if (module_resolve_symbol_global(symbolName, &symbol_address)) {
return symbol_address;
}
return 0;
}
static error_t start() {
elf_set_symbol_resolver(app_esp32_symbol_resolver);
return service_manager_add(&loader_service_manifest, /*auto_start=*/true);
}
static error_t stop() {
elf_set_symbol_resolver(nullptr);
return service_manager_remove(loader_service_manifest.id);
}
@@ -24,7 +38,7 @@ Module app_esp32_module = {
.stop = stop,
.drivers = nullptr,
.symbols = nullptr,
.internal = nullptr
.internal = nullptr,
};
}
+18
View File
@@ -9,4 +9,22 @@ tactility_add_module(app-module
PRIV_INCLUDE_DIRS private/
INCLUDE_DIRS include/
REQUIRES TactilityKernel service-module minitar
PRIV_REQUIRES TactilityKernelCpp
)
# 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 ()
# install.cpp resolves an installed binary's fixed bin/<platform>/<binary>.so path itself, so it
# needs the same platform-arch string app-posix-module's own CMakeLists.txt defines for its
# loader (the ESP32 equivalent, CONFIG_IDF_TARGET, comes for free from sdkconfig.h there).
if (NOT ESP_PLATFORM)
tactility_get_module_name(app-module MODULE_NAME)
target_compile_definitions(${MODULE_NAME} PRIVATE "TACTILITY_POSIX_ARCH=\"${CMAKE_SYSTEM_PROCESSOR}\"")
endif ()
@@ -0,0 +1,38 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once
#include <stdbool.h>
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
// <elf.h> is not guaranteed to exist under the ESP32 newlib toolchain
#define ELF_CLASS_32 1
#define ELF_CLASS_64 2
#define ELF_DATA_2LSB 1
#define ELF_TYPE_DYN 3
#define ELF_MACHINE_XTENSA 94
#define ELF_MACHINE_RISCV 243
#define ELF_MACHINE_X86_64 62
#define ELF_MACHINE_AARCH64 183
/** What a loader needs an ELF file's header to say before it will try to load it. */
struct ElfRequirements {
uint8_t elf_class; /**< ELF_CLASS_32 / ELF_CLASS_64 */
uint8_t data; /**< ELF_DATA_2LSB */
uint16_t type; /**< ELF_TYPE_DYN */
uint16_t machine; /**< ELF_MACHINE_XTENSA / _RISCV / _X86_64 / _AARCH64 */
};
/**
* Reads the first 20 bytes of @a path (e_ident, e_type, e_machine; identical offsets for
* ELF32 and ELF64) and checks the magic number and every field in @a requirements match.
* @return false if @a path can't be opened, is too short, or doesn't match
*/
bool elf_check_file(const char* path, const struct ElfRequirements* requirements);
#ifdef __cplusplus
}
#endif
+2 -2
View File
@@ -24,8 +24,8 @@ struct AppResultEventData {
uint32_t launch_id;
/** The child app instance's own AppMainFn/AppLoaderApi::run() return value. By convention:
* 0 = Ok, 1 = Cancelled, 2 = Error. Apps that need to hand back more than this (e.g. picked
* text, a path) expose their own "get last result" getter instead - see e.g.
* tt::app::inputdialog::getLastText(). */
* text, a path) write it to their own stdout instead, for the caller to read via an
* AppStream bound to it. See e.g. tt::app::inputdialog::start(). */
int32_t result;
};
+99
View File
@@ -0,0 +1,99 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once
/**
* This file contains functions to start and run apps.
* It differs from start.h by running apps directly from the specified location,
* instead of having to register them first via an AppManifest and the app manager.
*/
#include <app/manager.h>
#include <stdbool.h>
#ifdef __cplusplus
extern "C" {
#endif
/**
* Starts an app directly from @a location, without it having to be pre-registered via
* app_manager_add() first.
* Performs no checks of its own beyond what AppLoaderApi::load() itself rejects.
* @warning It's advised to validate @a location with app_is_executable() first
* @param[in] stack stack allocation config for the app's task; all-zero uses the scheduler's
* default depth/capability, same as an AppManifest that leaves AppManifest::stack zeroed
* @retval ERROR_NOT_FOUND no AppLoaderApi is registered for @a location.type
* @retval ERROR_NONE on success
*/
error_t app_execute(
struct AppLocation location,
struct AppStackConfig stack,
int argc,
const char* const argv[],
AppInstanceId* out_app_instance_id
);
/**
* Same as app_execute(), but as a modal child of @a parent_instance_id.
* See app_start_for_result()'s own doc for the result-delivery contract.
* @retval ERROR_NOT_FOUND no AppLoaderApi is registered for @a location.type
* @retval ERROR_NONE on success
*/
error_t app_execute_for_result(
struct AppLocation location,
struct AppStackConfig stack,
int argc,
const char* const argv[],
AppInstanceId parent_instance_id,
AppInstanceId* out_app_instance_id
);
/**
* Same as app_execute(), but installs @a bindings into the new instance's fd table before its
* task begins executing. See app_start_with_streams()'s own doc for stream ownership.
* @param[in] bindings see app_start_with_streams()
* @retval ERROR_NOT_FOUND no AppLoaderApi is registered for @a location.type
* @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_execute_with_streams(
struct AppLocation location,
struct AppStackConfig stack,
int argc,
const char* const argv[],
const struct AppStreamBinding* bindings,
size_t binding_count,
AppInstanceId* out_app_instance_id
);
/**
* Combines app_execute_for_result() and app_execute_with_streams().
* @param[in] bindings see app_start_with_streams()
* @retval ERROR_NOT_FOUND no AppLoaderApi is registered for @a location.type
* @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_execute_for_result_with_streams(
struct AppLocation location,
struct AppStackConfig stack,
int argc,
const char* const argv[],
const struct AppStreamBinding* bindings,
size_t binding_count,
AppInstanceId parent_instance_id,
AppInstanceId* out_app_instance_id
);
/**
* Reports whether @a location is runnable on this target: the extension and header a loader
* requires (e.g. an ELF's class/data/type/machine), not whether it lives anywhere in particular.
* Any executable app is runnable from any path. Cheap enough to call while listing a directory.
* @return false if @a location can't be run, or if no AppLoaderApi is registered for its type
*/
bool app_is_executable(struct AppLocation location);
#ifdef __cplusplus
}
#endif
+52
View File
@@ -0,0 +1,52 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once
#include <stddef.h>
#include <stdint.h>
#include <sys/types.h>
#include <tactility/error.h>
#include <tactility/freertos/freertos.h>
#ifdef __cplusplus
extern "C" {
#endif
/** Which readiness condition to block for in AppFileOps::await(). */
typedef enum {
APP_FILE_WAIT_READABLE,
APP_FILE_WAIT_WRITABLE,
} AppFileWait;
/** Bits returned by AppFileOps::poll(). */
#define APP_FILE_READABLE (1u << 0)
#define APP_FILE_WRITABLE (1u << 1)
/**
* Operations table for one kind of file-like object (stream, file, device, ...). Every function
* takes the type-erased `object` a concrete AppFile instance was constructed with.
*/
struct AppFileOps {
ssize_t (*read)(void* object, void* buffer, size_t size);
ssize_t (*write)(void* object, const void* buffer, size_t size);
error_t (*close)(void* object);
error_t (*await)(void* object, AppFileWait wait, TickType_t timeout);
/** @return bitmask of APP_FILE_READABLE / APP_FILE_WRITABLE. */
uint32_t (*poll)(void* object);
/** Optional (may be NULL). Called by app_fd_table_get_and_retain() atomically with the fd
* lookup, before handing `object` to a caller about to dispatch into it, so a concurrent
* teardown of `object` can wait for that dispatch to finish instead of racing it. The caller
* calls release() once done, regardless of what the dispatched call returned. */
void (*retain)(void* object);
void (*release)(void* object);
};
/** A file-descriptor-table entry: an operations table paired with the object it operates on. */
struct AppFile {
const struct AppFileOps* ops;
void* object;
};
#ifdef __cplusplus
}
#endif
+15 -14
View File
@@ -12,8 +12,8 @@ extern "C" {
/**
* Computes the install directory for @a app_id (does not check whether anything is actually
* installed there).
* @param[out] path always NULL-terminated on return, even on failure (empty string if
* @a path_size == 0 - nothing is written in that case; otherwise at least "" is written)
* @param[out] path always NULL-terminated on return, even on failure. Empty when @a path_size
* is 0, since nothing is written in that case.
* @retval ERROR_NONE on success
* @retval ERROR_BUFFER_OVERFLOW @a path_size is too small to hold the path (including the
* NULL terminator)
@@ -22,14 +22,15 @@ extern "C" {
error_t app_get_install_path(const char* app_id, char* path, size_t path_size);
/**
* Installs an app from a tarball at @a source_path: extracts it into the app install directory,
* parses the extracted manifest.properties (see app/metadata.h) to determine its id, then
* registers it with app_manager_add() as an AppLocation{APP_LOCATION_PATH, <install dir>} app.
* If an app with the same id is already installed (via a previous app_install() call), it is
* uninstalled first - stopped if running, its old install directory removed - before the new
* one takes its place.
* @param[in] source_path path to a tar file containing the app (must have manifest.properties
* at its root)
* Installs a package from a tarball at @a source_path: extracts it into the package's install
* directory, parses the extracted manifest.properties (see app/package_manifest.h) into a
* PackageManifest and one or more AppManifestBindings, then registers each with app_manager_add()
* as an AppLocation{APP_LOCATION_PATH, <binary path>} app.
* If a package with the same id is already installed (via a previous app_install() call), it is
* uninstalled first: every one of its apps stopped if running, then its old install directory
* removed, before the new one takes its place.
* @param[in] source_path path to a tar file containing the package (must have
* manifest.properties at its root)
* @retval ERROR_NONE on success
* @retval ERROR_NOT_FOUND @a source_path doesn't exist / can't be read
* @retval ERROR_INVALID_ARGUMENT the tarball has no valid manifest.properties at its root
@@ -37,11 +38,11 @@ error_t app_get_install_path(const char* app_id, char* path, size_t path_size);
error_t app_install(const char* source_path);
/**
* Uninstalls a previously app_install()-ed app: stops it if currently running, deletes its
* install directory, and unregisters it (app_manager_remove()).
* @param[in] app_id the id the app was installed under (AppMetadata::app_id)
* Uninstalls a previously app_install()-ed package: stops every one of its apps if currently
* running, deletes its install directory, and unregisters all of them (app_manager_remove()).
* @param[in] app_id the package id it was installed under (PackageManifest::id)
* @retval ERROR_NONE on success
* @retval ERROR_NOT_FOUND no such app was installed via app_install()
* @retval ERROR_NOT_FOUND no such package was installed via app_install()
*/
error_t app_uninstall(const char* app_id);
+35
View File
@@ -0,0 +1,35 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once
#include <stddef.h>
#include <sys/types.h>
#ifdef __cplusplus
extern "C" {
#endif
#define STDIN_FILENO 0
#define STDOUT_FILENO 1
#define STDERR_FILENO 2
/** Fixed size of an app instance's fd table. FD allocation uses the lowest unused index >= 3. */
#define APP_MAX_FDS 16
/**
* FD-table dispatch for read()/write()/close(). When the calling task belongs to a running app
* instance and @a fd is one that instance bound/allocated itself, it's looked up in that
* instance's own fd table. Otherwise (no app instance, e.g. a kernel service task; or an app
* instance's own fd that was never bound through app-module, e.g. a real file from fopen()/
* open(), which this layer never intercepts) @a fd is a real underlying fd and the call falls
* through to the real syscall unchanged.
* @return number of bytes transferred, 0 on EOF (read) or a closed peer (write), or -1 with
* errno set on failure. This is the fd-layer translation of AppFileOps::read()/write()'s ssize_t
* and AppFileOps::close()'s error_t.
*/
ssize_t app_io_read(int fd, void* buffer, size_t size);
ssize_t app_io_write(int fd, const void* buffer, size_t size);
int app_io_close(int fd);
#ifdef __cplusplus
}
#endif
+11 -12
View File
@@ -1,10 +1,11 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once
#include <app/manifest.h>
#include <tactility/error.h>
#include <stdint.h>
#include "location.h"
#include <app/manifest.h>
#include <stdbool.h>
#include <stdint.h>
#include <tactility/error.h>
#ifdef __cplusplus
extern "C" {
@@ -18,15 +19,8 @@ extern "C" {
* APP_LOCATION_PATH must register under. Implemented by a platform module (e.g. app-esp32-module). */
#define APP_LOADER_PATH_SERVICE_ID "app-loader-path"
/**
* Entry point signature for an APP_LOCATION_MEMORY app: a function linked directly into this
* firmware binary. Called on the dedicated task app-module's scheduler spawns for this instance,
* blocking for the app's whole lifetime - same contract as an external app's main(). Use
* app_scheduler_current_app_id() to identify this running instance (e.g. with
* app_event_subscribe()/window_manager_create()/etc.). The instance closes when this function
* returns - no separate call is needed.
* AppManifest::location.location holds this cast to void*.
*/
/** Entry point signature for an APP_LOCATION_MEMORY app.
* AppManifest::location.location holds this cast to void*. */
typedef int32_t (*AppMainFn)(int argc, char* argv[]);
typedef void* AppRuntime;
@@ -53,6 +47,11 @@ struct AppLoaderApi {
/** Releases whatever load() allocated. Called after run() returns. */
void (*unload)(AppRuntime runtime);
/**
* Reports whether this loader could load and run whatever @a location points at, without actually loading it.
*/
bool (*is_executable)(struct AppLocation location);
};
#ifdef __cplusplus
+49 -38
View File
@@ -3,6 +3,8 @@
#include <app/instance.h>
#include <app/manifest.h>
#include <app/package_manifest.h>
#include <app/stream.h>
#include <tactility/error.h>
@@ -44,47 +46,56 @@ typedef void (*AppManifestVisitorFn)(const struct AppManifest* manifest, void* c
void app_manager_for_each_manifest(AppManifestVisitorFn visitor, void* context);
/**
* Starts a new instance of the app registered under @a id. Every app instance gets its own
* dedicated task for its entire lifetime - starting an app never asks any other app to give up
* its task, and multiple instances (of the same or different apps) can be Active at once.
* @param[in] id the manifest id to start
* @param[out] out_app_instance_id the id of the new app instance
* @retval ERROR_NOT_FOUND no manifest with this id is registered, or no AppLoaderApi is registered
* Registers a package for enumeration via app_manager_for_each_package(). Separate from
* registering its apps - the caller still calls app_manager_add() for each AppManifest.
* @param[in] app_ids the ids of the AppManifest(s) this package registered
* @retval ERROR_INVALID_ARGUMENT a package with the same id is already registered
* @retval ERROR_NONE on success
*/
error_t app_manager_start(const char* id, AppInstanceId* out_app_instance_id);
error_t app_manager_add_package(const struct PackageManifest* package, const char* const* app_ids, size_t app_id_count);
/**
* Same as app_manager_start(), but also passes @a argc/@a argv to the new instance's own main
* function (see app/loader.h's AppMainFn) - modelled on a C program's main(argc, argv). For
* regular (non-modal) navigations that need to pass data to the target app (e.g. "show details
* for this app id") without expecting a result back.
* @param[in] argv @a argc strings; app-module makes its own deep copy before returning, so
* @a argv and the strings it points to may be freed/go out of scope immediately after this call
* returns (e.g. safe to pass a stack-local array of a caller's own std::string::c_str()s).
*/
error_t app_manager_start_with_parameters(const char* id, int argc, const char* const argv[], AppInstanceId* out_app_instance_id);
/**
* Starts @a id as a modal child of @a parent_instance_id, for the purpose of receiving a
* result. The parent keeps running (window_manager's own multi-window stack handles burying its
* window while the child is shown).
*
* When the child's task exits, an APP_EVENT_RESULT is delivered to @a parent_instance_id -
* result is whatever the child's AppMainFn/AppLoaderApi::run() returned - unless
* @a parent_instance_id is 0, in which case no result is delivered (fire-and-forget, for
* callers with no app_instance_id of their own). The parent is then responsible for calling
* app_manager_stop() on the child's instance id to fully reap it. Children that need to hand
* back more than an int32_t (e.g. picked text, a path) expose their own "get last result"
* getter for the parent to call after receiving the event - see e.g.
* tt::app::inputdialog::getLastText().
* @param[in] argv @a argc strings; app-module makes its own deep copy before returning (same as
* app_manager_start_with_parameters()), so @a argv and the strings it points to may be
* freed/go out of scope immediately after this call returns.
* @retval ERROR_NOT_FOUND no manifest with this id is registered, or no AppLoaderApi is registered
* Unregisters a previously-added package. Does not touch its apps' own registrations.
* @retval ERROR_NOT_FOUND no package with this id is registered
* @retval ERROR_NONE on success
*/
error_t app_manager_start_for_result(const char* id, AppInstanceId parent_instance_id, int argc, const char* const argv[], AppInstanceId* out_app_instance_id);
error_t app_manager_remove_package(const char* package_id);
/**
* @param[out] out_package set to a copy of the package on success
* @retval ERROR_NOT_FOUND no package with this id is registered
* @retval ERROR_NONE on success
*/
error_t app_manager_find_package(const char* package_id, struct PackageManifest* out_package);
/** One registered package, handed to AppPackageVisitorFn - see app_manager_for_each_package(). */
struct AppPackage {
struct PackageManifest package;
/** How many entries @a app_ids points to. */
size_t app_id_count;
/** Valid only for the duration of the app_manager_for_each_package() call that produced
* this - copy out what's needed before returning from the visitor. */
const char* const* app_ids;
};
typedef void (*AppPackageVisitorFn)(const struct AppPackage* pkg, void* context);
/**
* Calls @a visitor once for every registered package. Iteration order is unspecified.
* @warning Same threading contract as app_manager_for_each_manifest(): runs with an internal
* lock held - do not call any app_manager_*() function from inside @a visitor.
*/
void app_manager_for_each_package(AppPackageVisitorFn visitor, void* context);
/** One fd-to-stream binding for app_start_with_streams() (app/start.h). Every field is passed
* through to app_stream_subscribe() as-is; see its own doc for the ownership contracts. */
struct AppStreamBinding {
int producer_fd;
struct AppStream* stream;
void* buffer;
size_t buffer_capacity;
struct TaskEventGroup* event_group;
};
/**
* Stop an app instance permanently. Emits APP_EVENT_CLOSE and bound-waits for its task to exit
@@ -101,7 +112,7 @@ AppInstanceState app_manager_get_state(AppInstanceId app_instance_id);
/**
* @param[out] out_app_instance_id set to the instance id of the topmost currently-Active app -
* the most recently started of whichever instances are Active (a modal child launched via
* app_manager_start_for_result() stays Active alongside its parent while shown, so this
* app_start_for_result() (app/start.h) stays Active alongside its parent while shown, so this
* correctly picks the child, not the parent, while a dialog is up).
* @retval ERROR_NOT_FOUND no app is Active
* @retval ERROR_NONE on success
@@ -122,8 +133,8 @@ error_t app_manager_get_topmost_app_id(char* buffer, size_t buffer_size);
/**
* Registers @a path as a directory to scan for app manifests - each direct subdirectory of
* @a path is expected to hold a manifest.properties (see app/metadata.h), matching the layout
* app_install() creates ({install dir}/{app_id}/manifest.properties), though this is not
* @a path is expected to hold a manifest.properties (see app/package_manifest.h), matching the
* layout app_install() creates ({install dir}/{package id}/manifest.properties), though this is not
* install/uninstall - it only ever adds/removes manifest registrations, never touches files on
* disk or running instances. No-op if @a path is already registered. Does not scan immediately -
* call app_manager_install_path_scan() to do that.
+11 -6
View File
@@ -11,7 +11,10 @@ extern "C" {
#endif
// Character count, excluding null terminator
#define APP_ID_LENGTH 32
#define APP_MANIFEST_ID_LENGTH 32
// Character count, excluding null terminator
#define APP_MANIFEST_NAME_LENGTH 32
/** Broad classification of an app, used for grouping/launcher presentation. */
enum AppCategory {
@@ -45,10 +48,10 @@ struct AppStackConfig {
/** Describes a registrable app. One manifest exists per app id. */
struct AppManifest {
/** Unique app identifier. Should never be NULL. */
const char* id;
/** Human-readable name. Should never be NULL. */
const char* name;
/** Unique app identifier. Must be NULL-terminated. */
char id[APP_MANIFEST_ID_LENGTH + 1];
/** Human-readable name. Must be NULL-terminated. */
char name[APP_MANIFEST_NAME_LENGTH + 1];
enum AppCategory category;
struct AppLocation location;
/** Bitmask of AppManifestFlags. Most apps should leave this 0. */
@@ -57,7 +60,9 @@ struct AppManifest {
struct AppStackConfig stack;
};
bool app_id_is_valid(const char* id);
bool app_manifest_id_is_valid(const char* id);
bool app_manifest_name_is_valid(const char* name);
bool app_manifest_stack_size_is_valid(const char* value);
#ifdef __cplusplus
}
-74
View File
@@ -1,74 +0,0 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once
#include <tactility/error.h>
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
#define APP_METADATA_TARGET_SDK_LENGTH 16
#define APP_METADATA_APP_ID_LENGTH 32
#define APP_METADATA_APP_NAME_LENGTH 32
#define APP_METADATA_APP_VERSION_NAME_LENGTH 16
#define APP_METADATA_REQUIRES_DEVICE_ID_LENGTH 64
struct AppMetadata {
/**
* The SDK version that was used to compile this app. (e.g. "0.6.0")
* Must be NULL-terminated.
*/
char target_sdk[APP_METADATA_TARGET_SDK_LENGTH + 1];
/**
* The identifier by which the app is launched by the system and other apps.
* Must be NULL-terminated.
*/
char app_id[APP_METADATA_APP_ID_LENGTH + 1];
/**
* The user-readable name of the app. Used in UI.
* Must be NULL-terminated.
*/
char app_name[APP_METADATA_APP_NAME_LENGTH + 1];
/**
* The version as it is displayed to the user (e.g. "1.2.0")
* Must be NULL-terminated.
*/
char app_version_name[APP_METADATA_APP_VERSION_NAME_LENGTH + 1];
/** The technical version (must be incremented with new releases of the app) */
uint64_t app_version_code;
/**
* Comma-separated list of device ids the app is restricted to (e.g. "m5stack-tab5"), matching
* the folder names under Devices/. Empty means unrestricted.
* Must be NULL-terminated.
*/
char requires_device_id[APP_METADATA_REQUIRES_DEVICE_ID_LENGTH + 1];
/**
* Stack depth (in words) for the app's task. Optional; 0 means scheduler default.
* @warning Avoid default values: the default is conservative, which wastes memory.
*/
uint32_t stack_depth;
};
/**
* Parses a manifest.properties file at @a path into @a out_metadata, auto-detecting the V1
* (sectioned, e.g. "[app]id=...") or V2 (flat dot-notation, e.g. "app.id=...") format from its
* first line.
* @retval ERROR_NONE on success
* @retval ERROR_NOT_FOUND the file doesn't exist / couldn't be opened
* @retval ERROR_INVALID_ARGUMENT the file isn't a valid manifest, or a field's value doesn't fit
* @a out_metadata's fixed-size buffers
*/
error_t app_metadata_parse(const char* path, struct AppMetadata* out_metadata);
#ifdef __cplusplus
}
#endif
@@ -0,0 +1,88 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once
#include <app/manifest.h>
#include <tactility/error.h>
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
#define PACKAGE_MANIFEST_TARGET_SDK_LENGTH 16
#define PACKAGE_MANIFEST_ID_LENGTH 32
#define PACKAGE_MANIFEST_VERSION_NAME_LENGTH 16
#define PACKAGE_MANIFEST_REQUIRES_DEVICE_ID_LENGTH 64
/** Character count, excluding null terminator, for AppManifestBinding::binary. */
#define APP_MANIFEST_BINARY_LENGTH 31
/** Largest number of AppManifest entries package_manifest_parse() can produce from a single
* manifest.properties file. */
#define PACKAGE_MANIFEST_MAX_APP_MANIFESTS 32
/** A package-level manifest.properties: SDK/version metadata that applies to the whole package,
* not to any one of its (possibly several) apps. Not kept in memory at runtime - only used to
* create and cache the AppManifest(s) it describes. */
struct PackageManifest {
/**
* The package identifier (e.g. the install directory name). Distinct from any of its own
* AppManifest ids.
* Must be NULL-terminated.
*/
char id[PACKAGE_MANIFEST_ID_LENGTH + 1];
/**
* The package version as it is displayed to the user (e.g. "1.2.0")
* Must be NULL-terminated.
*/
char version_name[PACKAGE_MANIFEST_VERSION_NAME_LENGTH + 1];
/** The package's technical version (must be incremented with new releases). */
uint64_t version_code;
/**
* The SDK version that was used to compile this package. (e.g. "0.6.0")
* Must be NULL-terminated.
*/
char target_sdk[PACKAGE_MANIFEST_TARGET_SDK_LENGTH + 1];
/**
* Comma-separated list of device ids the package is restricted to (e.g. "m5stack-tab5"),
* matching the folder names under Devices/. Empty means unrestricted.
* Must be NULL-terminated.
*/
char requires_device_id[PACKAGE_MANIFEST_REQUIRES_DEVICE_ID_LENGTH + 1];
/** How many AppManifest entries this package's manifest.properties declared. */
uint32_t app_manifest_count;
};
/** Pairs a parsed AppManifest with the filename (without extension) it installs as under
* bin/<platform>/ - e.g. "main" resolves to bin/posix-x86_64/main.so. Not kept anywhere at
* runtime - same lifetime as PackageManifest, only used to hand parse results to the installer/
* scanner, which resolve `binary` into AppManifest::location::location. */
struct AppManifestBinding {
struct AppManifest manifest;
char binary[APP_MANIFEST_BINARY_LENGTH + 1];
};
/**
* Parses a manifest.properties file at @a path (flat dot-notation, e.g. "app.id=...") into
* @a out_package and @a out_bindings.
* @param[out] out_bindings written with up to @a bindings_capacity entries (see
* PackageManifest::app_manifest_count for how many)
* @param[in] bindings_capacity the capacity of @a out_bindings
* @retval ERROR_NONE on success
* @retval ERROR_NOT_FOUND the file doesn't exist / couldn't be opened
* @retval ERROR_INVALID_ARGUMENT the file isn't a valid manifest, or a field's value doesn't fit
* its fixed-size buffer
* @retval ERROR_BUFFER_OVERFLOW the manifest declares more apps than @a bindings_capacity
*/
error_t app_package_manifest_parse(const char* path, struct PackageManifest* out_package, struct AppManifestBinding* out_bindings, size_t bindings_capacity);
#ifdef __cplusplus
}
#endif
+76
View File
@@ -0,0 +1,76 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once
#include <app/manager.h>
/**
* This file contains functions to start and run apps that were registered to the app manager.
* It differs from execute.h which runs executables from a specific path.
*/
#ifdef __cplusplus
extern "C" {
#endif
/**
* Starts @a id, a manifest already registered via app_manager_add(), passing @a argc/@a argv to
* the new instance's own main function (see app/loader.h's AppMainFn), modelled on a C program's
* main(argc, argv). For regular (non-modal) navigations that need to pass data to the target app
* (e.g. "show details for this app id") without expecting a result back.
* @param[in] argv @a argc strings; app-module makes its own deep copy before returning, so
* @a argv and the strings it points to may be freed/go out of scope immediately after this call
* returns (e.g. safe to pass a stack-local array of a caller's own std::string::c_str()s).
* @retval ERROR_NOT_FOUND no manifest with this id is registered, or no AppLoaderApi is registered
* @retval ERROR_NONE on success
*/
error_t app_start(const char* id, int argc, const char* const argv[], AppInstanceId* out_app_instance_id);
/**
* Starts @a id as a child of @a parent_instance_id, for the purpose of receiving a result.
*
* When the child's task exits, an APP_EVENT_RESULT is delivered to @a parent_instance_id.
* The result is whatever the child's AppMainFn/AppLoaderApi::run() returned, unless
* @a parent_instance_id is 0, in which case no result is delivered (fire-and-forget, for
* callers with no app_instance_id of their own). The parent is then responsible for calling
* app_manager_stop() on the child's instance id to fully reap it.
*
* @param[in] argv @a argc strings; app-module makes its own deep copy before returning (same as
* app_start()), so @a argv and the strings it points to may be
* freed/go out of scope immediately after this call returns.
* @retval ERROR_NOT_FOUND no manifest with this id is registered, or no AppLoaderApi is registered
* @retval ERROR_NONE on success
*/
error_t app_start_for_result(const char* id, int argc, const char* const argv[], AppInstanceId parent_instance_id, AppInstanceId* out_app_instance_id);
/**
* Same as app_start(), but installs @a bindings into the new instance's fd table before
* its task begins executing (e.g. a child's stdio, piped through parent-owned AppStreams; see
* app/stream.h). Writes the new instance's id into each bound stream's producer_id itself, since
* the caller cannot know it in advance.
* @param[in] bindings @a binding_count entries; each stream and buffer must stay alive (see
* app_stream_subscribe()) until unsubscribed or the child exits.
* @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_start_with_streams(const char* id, const struct AppStreamBinding* bindings, size_t binding_count, AppInstanceId* out_app_instance_id);
/**
* Combines app_start_for_result() and app_start_with_streams(): starts @a id as
* a modal child of @a parent_instance_id (see app_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_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.
* @param[in] argv see app_start_for_result().
* @param[in] bindings see app_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_start_for_result_with_streams(const char* id, int argc, const char* const argv[], const struct AppStreamBinding* bindings, size_t binding_count, AppInstanceId parent_instance_id, AppInstanceId* out_app_instance_id);
#ifdef __cplusplus
}
#endif
+115
View File
@@ -0,0 +1,115 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once
#include <app/file.h>
#include <app/instance.h>
#include <stddef.h>
#include <stdint.h>
#include <tactility/concurrent/mutex.h>
#include <tactility/concurrent/task_event_group.h>
#include <tactility/error.h>
#include <tactility/freertos/freertos.h>
#include <tactility/freertos/task.h>
#ifdef __cplusplus
extern "C" {
#endif
/** @warning This is internal data. Do not read/write to it directly. */
struct AppStreamBuffer {
uint8_t* data;
size_t capacity;
size_t read_pos;
size_t write_pos;
size_t count;
};
/**
* Buffered byte-oriented communication between one producer task and one consumer task. The
* consumer owns the AppStream object and its lifetime. See app_stream_subscribe().
*
* @warning This is internal data. Do not read/write to it directly.
*/
struct AppStream {
struct AppStreamBuffer buffer;
AppInstanceId producer_id;
TaskHandle_t producer_task;
/** fd this stream is installed at in producer_id's fd table; set by
* app_stream_subscribe()/app_start_with_streams(), used by
* app_stream_unsubscribe() to find it again. */
int producer_fd;
struct Mutex mutex;
/** Caller-owned group readiness is signalled on; see app_stream_subscribe(). */
struct TaskEventGroup* event_group;
uint32_t readable_bit;
uint32_t writable_bit;
bool closed;
/** Count of AppFileOps calls currently executing against this stream; app_stream_unsubscribe()
* waits for this to reach 0 before destructing `mutex`, since a call already blocked in
* app_stream_await() when unsubscribe starts only wakes (it doesn't vanish) when closed. */
int active_operations;
};
/**
* Registers @a stream as the file-like object bound to @a producer_id's fd table at
* @a producer_fd, claiming two bits from @a event_group for its readable/writable readiness.
* There is at most one subscriber for a given app fd. Subscribing over an existing one
* atomically closes it first (any task blocked on it wakes; already-buffered bytes remain
* readable until drained) and installs @a stream in its place.
* @param[in,out] stream caller-owned. Storage must remain valid until app_stream_unsubscribe()
* returns. That is the only operation guaranteeing both that the fd-table binding is gone and
* that no AppFileOps call is still executing against @a stream; app_stream_close() alone does
* neither.
* @param[in] buffer ring buffer storage; caller-owned, same validity requirement as @a stream.
* @param[in] buffer_capacity size of @a buffer in bytes.
* @param[in] event_group caller-owned; must outlive @a stream (i.e. be destructed only after
* app_stream_unsubscribe()). Lets a task wait on this stream's readiness together with other
* event sources sharing the same group via task_event_group_wait()/task_event_group_wait_any().
* @retval ERROR_NOT_FOUND no instance with this id is running
* @retval ERROR_OUT_OF_RANGE @a producer_fd is out of range
* @retval ERROR_RESOURCE @a event_group has no free bits left to claim
* @retval ERROR_NONE on success
*/
error_t app_stream_subscribe(struct AppStream* stream, void* buffer, size_t buffer_capacity, struct TaskEventGroup* event_group, AppInstanceId producer_id, int producer_fd);
/**
* Removes @a stream's binding from whichever fd table it was installed in (further use of that
* fd then fails), wakes and waits for every AppFileOps call currently in flight against
* @a stream to finish, then releases its two bits back to the event_group given to
* app_stream_subscribe() and destructs its internal mutex. Only after this returns is
* @a stream's storage safe to free or reuse.
*/
error_t app_stream_unsubscribe(struct AppStream* stream);
/**
* Blocks until @a stream becomes readable/writable (per @a wait) or @a timeout elapses. Checks
* the current state directly before blocking, so a permanently-true condition (e.g. closed)
* keeps returning immediately on every call, even though the underlying event bit itself is
* one-shot (task_event_group_wait() clears a matched bit on exit).
* @retval ERROR_NONE the condition is true
* @retval ERROR_TIMEOUT @a timeout elapsed
* @retval ERROR_ISR_STATUS called from an ISR
*/
error_t app_stream_await(struct AppStream* stream, AppFileWait wait, TickType_t timeout);
/** Non-blocking: copies up to @a buffer_size currently-available bytes out of @a stream. */
size_t app_stream_read(struct AppStream* stream, void* buffer, size_t buffer_size);
/** Non-blocking: copies up to @a buffer_size bytes into @a stream, as much as currently fits.
* Copies nothing and returns 0 once @a stream is closed. */
size_t app_stream_write(struct AppStream* stream, const void* buffer, size_t buffer_size);
/**
* Marks @a stream closed: wakes any task blocked in app_stream_await(), and causes the other side
* to observe EOF (reader, once drained) or a write error (writer). Safe to call while another
* task may be blocked on @a stream, unlike app_stream_unsubscribe() (see its own doc); does not
* by itself make @a stream's storage safe to free.
*/
error_t app_stream_close(struct AppStream* stream);
#ifdef __cplusplus
}
#endif
@@ -0,0 +1,60 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once
#include <cstring>
#include <new>
/**
* Deep-copies @a argv (@a argc <= 0 => NULL, matching "no parameters"). Caller passes the result
* to app_scheduler_start(), which takes ownership regardless of outcome.
* @return NULL if @a argc <= 0 (no parameters), or if allocation failed. For @a argc > 0,
* these are the only cases that produce NULL, so a caller can tell them apart by its own
* already-known @a argc: NULL back from a positive @a argc always means allocation failed.
* All partial allocations are freed before returning NULL, so failure never leaks memory.
*/
inline char** app_arguments_copy(int argc, const char* const argv[]) {
if (argc <= 0) {
return nullptr;
}
auto* copy = new (std::nothrow) char*[argc + 1];
if (copy == nullptr) {
return nullptr;
}
int copied = 0;
for (; copied < argc; copied++) {
size_t length = strlen(argv[copied]);
copy[copied] = new (std::nothrow) char[length + 1];
if (copy[copied] == nullptr) {
break;
}
memcpy(copy[copied], argv[copied], length + 1);
}
if (copied < argc) {
for (int i = 0; i < copied; i++) {
delete[] copy[i];
}
delete[] copy;
return nullptr;
}
copy[argc] = nullptr;
return copy;
}
/**
* Frees a deep-copied argv previously built by app_arguments_copy(): each individually
* heap-allocated string, then the array itself. Safe to call with count == 0 / values == nullptr
* (no-op).
*/
inline void app_arguments_free(int count, char** values) {
if (values == nullptr) {
return;
}
for (int i = 0; i < count; i++) {
delete[] values[i];
}
delete[] values;
}
@@ -0,0 +1,22 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once
#ifdef ESP_PLATFORM
#include <sdkconfig.h>
#endif
#include <string>
// Resolves an AppManifestBinding::binary filename (without extension) to its installed path
// under this fixed, predictable location, mirroring what each platform's own loader
// (app_posix_loader_service.cpp / app_esp32_loader_service.cpp) already resolves a plain
// ".so"/".elf" AppLocation::location straight through unchanged - so setting location.location
// to this exact path means neither loader needs to guess which of a package's several binaries
// an AppManifest refers to.
inline std::string app_resolve_binary_path(const std::string& install_dir, const std::string& binary) {
#ifdef ESP_PLATFORM
return install_dir + "/bin/" CONFIG_IDF_TARGET "/" + binary + ".elf";
#else
return install_dir + "/bin/posix-" TACTILITY_POSIX_ARCH "/" + binary + ".so";
#endif
}
@@ -0,0 +1,98 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once
#include <app/file.h>
#include <app/io.h>
#include <stdbool.h>
#include <tactility/concurrent/mutex.h>
#include <tactility/error.h>
/**
* One fd's worth of state in an AppFdTable. `file` is valid only while `in_use` is true.
* `ever_used` is set once `in_use` is ever set true (including the default stdio binding at
* construct time) and never cleared, even once `in_use` goes back to false on close. This is
* what lets app_fd_table_is_app_owned() distinguish "this fd number belongs to us, it's just
* currently closed" (an app-level EBADF) from "app-module has never touched this fd number" (a
* real underlying fd, e.g. from fopen()/open(), that a caller should fall through on).
*
* @warning This is internal data. Do not read/write to it directly.
*/
struct AppFdSlot {
struct AppFile file;
bool in_use;
bool ever_used;
};
/**
* Every app instance's fd table (see AppInstanceRecord). `mutex` guards `slots` only; it is
* never held while calling into an AppFile's ops (dispatch copies the AppFile out under the lock
* first), since those calls belong to independently synchronized objects (AppStream's own
* mutex/event group) and may run concurrently with a close()/bind() replacing an unrelated slot.
*
* `shutting_down` is set under `mutex` by app_fd_table_teardown() before it destructs `mutex`,
* and checked (also under `mutex`) by every other entry point. Callers are expected to
* serialize against teardown externally (see app_stream_subscribe()/app_stream_unsubscribe()),
* so a check() failure here means that external synchronization broke, not a condition to
* handle gracefully.
*
* @warning This is internal data. Do not read/write to it directly.
*/
struct AppFdTable {
struct AppFdSlot slots[APP_MAX_FDS];
struct Mutex mutex;
bool shutting_down;
};
#ifdef __cplusplus
extern "C" {
#endif
/** Initializes every fd to the null device (see app/private/null_device.h) and constructs `mutex`. */
void app_fd_table_construct(struct AppFdTable* table);
/** Closes every still-open fd (via AppFileOps::close()) and destructs `mutex`. */
void app_fd_table_teardown(struct AppFdTable* table);
/**
* Installs {ops, object} at @a fd, closing whatever was previously there first (the null device
* counts as "previously there" for fds 0-2, so this doubles as their initial stdio binding).
* @retval ERROR_OUT_OF_RANGE @a fd is outside [0, APP_MAX_FDS)
*/
error_t app_fd_table_bind(struct AppFdTable* table, int fd, const struct AppFileOps* ops, void* object);
/**
* Installs {ops, object} at the lowest unused fd >= 3.
* @retval ERROR_RESOURCE the table is full
*/
error_t app_fd_table_allocate(struct AppFdTable* table, const struct AppFileOps* ops, void* object, int* out_fd);
/** @return true and fills @a out_file if @a fd is currently in use, false otherwise. */
bool app_fd_table_get(struct AppFdTable* table, int fd, struct AppFile* out_file);
/**
* Like app_fd_table_get(), but also calls the returned AppFile's AppFileOps::retain() (if any)
* while still holding `mutex`, atomically with the lookup. A caller that gets true back can
* therefore never have @a fd closed/torn down out from under it between this call and actually
* dispatching into the returned AppFile. Caller must call AppFileOps::release() once done
* dispatching.
*/
bool app_fd_table_get_and_retain(struct AppFdTable* table, int fd, struct AppFile* out_file);
/**
* Closes @a fd (via AppFileOps::close()) and frees the slot for reuse.
* @retval ERROR_NOT_FOUND @a fd is out of range or not currently in use
*/
error_t app_fd_table_close(struct AppFdTable* table, int fd);
/**
* @return true if @a fd is currently in use, or was in the past (even if since closed). This is
* whether the table claims @a fd as one of its own, regardless of its current state. False means
* @a fd is a real underlying fd this table has never bound or allocated.
*/
bool app_fd_table_is_app_owned(struct AppFdTable* table, int fd);
#ifdef __cplusplus
}
#endif
+20 -16
View File
@@ -3,6 +3,10 @@
#include <app/instance.h>
#include <app/manifest.h>
#include <app/package_manifest.h>
#include <app/private/fd_table.h>
#include <TactilityCpp/Allocator.h>
#include <tactility/concurrent/mutex.h>
#include <tactility/freertos/freertos.h>
@@ -12,6 +16,7 @@
#include <stdint.h>
#include <string>
#include <unordered_map>
#include <vector>
/**
* A dedicated completion signal(1) for one app instance's task, given as the
@@ -37,22 +42,35 @@ struct AppCompletionSignal {
/** A registered/running app instance, as tracked internally by app-module. */
struct AppInstanceRecord {
uint32_t id;
/** NULL for an instance started via app_execute() (app/execute.h; no manifest involved). */
const AppManifest* manifest;
AppInstanceState state;
/** The FreeRTOS task currently executing AppLoaderApi::run() for this instance; NULL when not running. */
TaskHandle_t task;
/** 0 for a top-level launch (app_manager_start()). Non-zero for a modal child launched via
* app_manager_start_for_result() - the instance that receives this child's APP_EVENT_RESULT. */
/** 0 for a top-level launch (app_start()). Non-zero for a modal child launched via
* app_start_for_result() - the instance that receives this child's APP_EVENT_RESULT. */
uint32_t parent_id = 0;
/** This instance's completion signal - see AppCompletionSignal. Set once by
* app_scheduler_start(), never reassigned. */
AppCompletionSignal* completion = nullptr;
/** This instance's fd table. Constructed by app_manager_start_internal() before insertion
* into AppLedger::instances, torn down (every open fd closed) when the instance's task exits. */
AppFdTable fd_table {};
};
/** A registered installed package - see app_manager_add_package() (app/manager.h). */
struct AppPackageRecord {
struct PackageManifest package;
std::vector<std::string> app_ids;
};
struct AppLedger {
std::unordered_map<std::string, const AppManifest*> manifests;
// OptExternalAllocator: bigger entries than `manifests`, and unlike `instances` isn't on the app start/stop hot path.
std::unordered_map<std::string, AppPackageRecord, std::hash<std::string>, std::equal_to<std::string>, tt::OptExternalAllocator<std::pair<const std::string, AppPackageRecord>>> packages;
std::unordered_map<uint32_t, AppInstanceRecord> instances;
uint32_t next_instance_id = 1;
Mutex mutex {};
@@ -65,17 +83,3 @@ inline AppLedger& app_ledger() {
static AppLedger ledger;
return ledger;
}
/**
* Frees a deep-copied argv previously built by app_manager_start_with_parameters()/app_manager_start_for_result():
* each individually heap-allocated string, then the array itself. Safe to call with count == 0 values == nullptr (no-op).
*/
inline void app_ledger_free_arguments(int count, char** values) {
if (values == nullptr) {
return;
}
for (int i = 0; i < count; i++) {
delete[] values[i];
}
delete[] values;
}

Some files were not shown because too many files have changed in this diff Show More