Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/actions/build-sdk-posix/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ runs:
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
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:
Expand Down
2 changes: 1 addition & 1 deletion .github/actions/build-sdk/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ runs:
with:
esp_idf_version: v6.1
target: ${{ inputs.arch }}
command: export TACTILITY_SDK_PATH=../../test_sdk && cd Tests/SdkIntegration && python tactility.py build -a ${{ 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:
Expand Down
25 changes: 25 additions & 0 deletions Buildscripts/CDN/upload-sdk-files.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import io
import json
import os
import re
import sys
import boto3

Expand All @@ -22,6 +25,15 @@ def exit_with_error(message):
print_error(message)
sys.exit(1)

TOOL_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "..", "tactility.py")

def create_tool_json():
with open(TOOL_PATH, "r") as file:
match = re.search(r'^ttbuild_version = "([^"]+)"', file.read(), re.MULTILINE)
if match is None:
exit_with_error(f"ttbuild_version not found in {TOOL_PATH}")
return json.dumps({"version": match.group(1)}, indent=2).encode("utf-8")

def main(path: str, version: str, cloudflare_account_id, cloudflare_token_name: str, cloudflare_token_value: str, index_only: bool):
if not os.path.exists(path):
exit_with_error(f"Path not found: {path}")
Expand All @@ -45,6 +57,19 @@ def main(path: str, version: str, cloudflare_account_id, cloudflare_token_name:
except Exception as e:
exit_with_error(f"Failed to upload {file_name}: {str(e)}")
counter += 1
if not index_only:
tool_object_path = f"sdk/{version}/tactility.py"
print(f"Uploading tactility.py to {tool_object_path}")
try:
s3.upload_file(TOOL_PATH, "tactility", tool_object_path)
except Exception as e:
exit_with_error(f"Failed to upload tactility.py: {str(e)}")
tool_json_object_path = f"sdk/{version}/tactility.py.json"
print(f"Uploading tactility.py.json to {tool_json_object_path}")
try:
s3.upload_fileobj(io.BytesIO(create_tool_json()), "tactility", tool_json_object_path)
except Exception as e:
exit_with_error(f"Failed to upload tactility.py.json: {str(e)}")

if __name__ == "__main__":
print("Tactility CDN SDK Uploader")
Expand Down
24 changes: 24 additions & 0 deletions Buildscripts/TactilitySDK/sdkconfig.app.esp32
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# FreeRTOS defaults (must match wit OS)
CONFIG_FREERTOS_HZ=1000
CONFIG_FREERTOS_TASK_NOTIFICATION_ARRAY_ENTRIES=2
CONFIG_FREERTOS_SMP=n
CONFIG_FREERTOS_UNICORE=n
CONFIG_FREERTOS_TIMER_TASK_STACK_DEPTH=4096
CONFIG_FREERTOS_USE_TRACE_FACILITY=y

# FatFS defaults (must match with OS)
CONFIG_FATFS_LFN_HEAP=y
CONFIG_FATFS_VOLUME_COUNT=3

# App-specific
CONFIG_PARTITION_TABLE_SINGLE_APP=y
CONFIG_ESP_SYSTEM_MEMPROT_FEATURE_LOCK=n

# Platform
CONFIG_IDF_TARGET="esp32"

# Force newlib for IDF 6.x and higher
CONFIG_LIBC_NEWLIB=y

# VFS (must match with OS): exposes termios.h and tcgetattr()/tcsetattr()
CONFIG_VFS_SUPPORT_TERMIOS=y
24 changes: 24 additions & 0 deletions Buildscripts/TactilitySDK/sdkconfig.app.esp32c6
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# FreeRTOS defaults (must match wit OS)
CONFIG_FREERTOS_HZ=1000
CONFIG_FREERTOS_TASK_NOTIFICATION_ARRAY_ENTRIES=2
CONFIG_FREERTOS_SMP=n
CONFIG_FREERTOS_UNICORE=n
CONFIG_FREERTOS_TIMER_TASK_STACK_DEPTH=4096
CONFIG_FREERTOS_USE_TRACE_FACILITY=y

# FatFS defaults (must match with OS)
CONFIG_FATFS_LFN_HEAP=y
CONFIG_FATFS_VOLUME_COUNT=3

# App-specific
CONFIG_PARTITION_TABLE_SINGLE_APP=y
CONFIG_ESP_SYSTEM_MEMPROT_FEATURE_LOCK=n

# Platform
CONFIG_IDF_TARGET="esp32c6"

# Force newlib for IDF 6.x and higher
CONFIG_LIBC_NEWLIB=y

# VFS (must match with OS): exposes termios.h and tcgetattr()/tcsetattr()
CONFIG_VFS_SUPPORT_TERMIOS=y
24 changes: 24 additions & 0 deletions Buildscripts/TactilitySDK/sdkconfig.app.esp32p4
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# FreeRTOS defaults (must match wit OS)
CONFIG_FREERTOS_HZ=1000
CONFIG_FREERTOS_TASK_NOTIFICATION_ARRAY_ENTRIES=2
CONFIG_FREERTOS_SMP=n
CONFIG_FREERTOS_UNICORE=n
CONFIG_FREERTOS_TIMER_TASK_STACK_DEPTH=4096
CONFIG_FREERTOS_USE_TRACE_FACILITY=y

# FatFS defaults (must match with OS)
CONFIG_FATFS_LFN_HEAP=y
CONFIG_FATFS_VOLUME_COUNT=3

# App-specific
CONFIG_PARTITION_TABLE_SINGLE_APP=y
CONFIG_ESP_SYSTEM_MEMPROT_FEATURE_LOCK=n

# Platform
CONFIG_IDF_TARGET="esp32p4"

# Force newlib for IDF 6.x and higher
CONFIG_LIBC_NEWLIB=y

# VFS (must match with OS): exposes termios.h and tcgetattr()/tcsetattr()
CONFIG_VFS_SUPPORT_TERMIOS=y
24 changes: 24 additions & 0 deletions Buildscripts/TactilitySDK/sdkconfig.app.esp32s3
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# FreeRTOS defaults (must match wit OS)
CONFIG_FREERTOS_HZ=1000
CONFIG_FREERTOS_TASK_NOTIFICATION_ARRAY_ENTRIES=2
CONFIG_FREERTOS_SMP=n
CONFIG_FREERTOS_UNICORE=n
CONFIG_FREERTOS_TIMER_TASK_STACK_DEPTH=4096
CONFIG_FREERTOS_USE_TRACE_FACILITY=y

# FatFS defaults (must match with OS)
CONFIG_FATFS_LFN_HEAP=y
CONFIG_FATFS_VOLUME_COUNT=3

# App-specific
CONFIG_PARTITION_TABLE_SINGLE_APP=y
CONFIG_ESP_SYSTEM_MEMPROT_FEATURE_LOCK=n

# Platform
CONFIG_IDF_TARGET="esp32s3"

# Force newlib for IDF 6.x and higher
CONFIG_LIBC_NEWLIB=y

# VFS (must match with OS): exposes termios.h and tcgetattr()/tcsetattr()
CONFIG_VFS_SUPPORT_TERMIOS=y
19 changes: 19 additions & 0 deletions Buildscripts/release-sdk-esp32.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import os
import glob
import shutil
import subprocess
import sys
import importlib.util
Expand Down Expand Up @@ -57,6 +58,23 @@ def add_module(target_path, module_name):
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 get_idf_target():
with open("sdkconfig", "r") as f:
for line in f:
if line.startswith("CONFIG_IDF_TARGET="):
return line.split('=')[1].strip().strip('"')
print("Error: CONFIG_IDF_TARGET not found in sdkconfig")
sys.exit(1)

def add_app_sdkconfig(target_path):
"""Bundles the sdkconfig that apps must build with for this SDK's target."""
filename = f"sdkconfig.app.{get_idf_target()}"
src = os.path.join('Buildscripts', 'TactilitySDK', filename)
if not os.path.isfile(src):
print(f"Error: {src} does not exist")
sys.exit(1)
shutil.copy2(src, os.path.join(target_path, filename))

def main():
if len(sys.argv) < 2:
print("Usage: release-sdk-esp32.py [target_path]")
Expand Down Expand Up @@ -117,6 +135,7 @@ def main():
# Final scripts - copied verbatim
shared.generate_tactility_sdk_cmake(target_path, 'esp32')
shared.generate_tactility_sdk_top_cmakelists(target_path)
add_app_sdkconfig(target_path)

# Output ESP-IDF SDK version to file
with open(os.path.join(target_path, "idf-version.txt"), "w") as f:
Expand Down
28 changes: 27 additions & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ if (DEFINED ENV{ESP_IDF_VERSION})
# Wraps newlib's reentrant syscall stubs, not the plain read()/write()/close() newlib itself.
# Implements as thin wrappers around them. newlib's own stdio (fflush()'s buffer-flush path in particular)
# calls these _r stubs directly, bypassing the plain names entirely.
# See Modules/app-module/source/stdio_wrap.cpp's own comment for the exact call chain.
# See Modules/app-module/source/stdio_wrap_esp32.cpp's own comment for the exact call chain.
idf_build_set_property(LINK_OPTIONS "-Wl,--wrap=_read_r" APPEND)
idf_build_set_property(LINK_OPTIONS "-Wl,--wrap=_write_r" APPEND)
idf_build_set_property(LINK_OPTIONS "-Wl,--wrap=_close_r" APPEND)
Expand All @@ -75,13 +75,39 @@ if (DEFINED ENV{ESP_IDF_VERSION})
idf_build_set_property(LINK_OPTIONS "-Wl,--wrap=getcwd" APPEND)
idf_build_set_property(LINK_OPTIONS "-Wl,--wrap=chdir" APPEND)

# fstat()/isatty() go through _fstat_r. poll() and tcgetattr()/tcsetattr() are defined directly (poll.c, vfs_calls.c).
# Wrapped so app fds report as polling-capable raw character devices.
idf_build_set_property(LINK_OPTIONS "-Wl,--wrap=_fstat_r" APPEND)
idf_build_set_property(LINK_OPTIONS "-Wl,--wrap=poll" APPEND)
idf_build_set_property(LINK_OPTIONS "-Wl,--wrap=tcgetattr" APPEND)
idf_build_set_property(LINK_OPTIONS "-Wl,--wrap=tcsetattr" APPEND)

# exit() from an app ends only that app (see app_scheduler_exit_current()) instead of aborting.
idf_build_set_property(LINK_OPTIONS "-Wl,--wrap=exit" APPEND)

# opendir()/readdir()/closedir() also define the plain names directly (vfs_calls.c).
# Wrapped so opendir("/") synthesizes a listing of own registered filesystems instead of ENOENT.
# See Platforms/platform-esp32/source/root_dir.cpp.
idf_build_set_property(LINK_OPTIONS "-Wl,--wrap=opendir" APPEND)
idf_build_set_property(LINK_OPTIONS "-Wl,--wrap=readdir" APPEND)
idf_build_set_property(LINK_OPTIONS "-Wl,--wrap=closedir" APPEND)

# mkdir() is an alias in vfs_calls.c. Wrapped so an existing mount root reports EEXIST.
# See Platforms/platform-esp32/source/mkdir.cpp.
idf_build_set_property(LINK_OPTIONS "-Wl,--wrap=mkdir" APPEND)

# Path-based VFS calls are aliases in vfs_calls.c. Wrapped so a NULL path fails with EFAULT instead of crashing.
# See Platforms/platform-esp32/source/vfs_null_path.cpp.
idf_build_set_property(LINK_OPTIONS "-Wl,--wrap=_open_r" APPEND)
idf_build_set_property(LINK_OPTIONS "-Wl,--wrap=_stat_r" APPEND)
idf_build_set_property(LINK_OPTIONS "-Wl,--wrap=_link_r" APPEND)
idf_build_set_property(LINK_OPTIONS "-Wl,--wrap=_unlink_r" APPEND)
idf_build_set_property(LINK_OPTIONS "-Wl,--wrap=_rename_r" APPEND)
idf_build_set_property(LINK_OPTIONS "-Wl,--wrap=truncate" APPEND)
idf_build_set_property(LINK_OPTIONS "-Wl,--wrap=access" APPEND)
idf_build_set_property(LINK_OPTIONS "-Wl,--wrap=utime" APPEND)
idf_build_set_property(LINK_OPTIONS "-Wl,--wrap=rmdir" 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)
idf_build_set_property(LINK_OPTIONS "-Wl,--wrap=lv_list_create" APPEND)
Expand Down
4 changes: 4 additions & 0 deletions Documentation/ideas.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@

## Medium Priority

- Implement signal/raise for terminal apps (for CTRL+C and window resize signals)
- Build the simulator with `-fno-exceptions` to match ESP32. First make app exit() work without C++ unwinding there:
scheduler.cpp's DeferredUnload relies on pthread_exit()'s forced unwind running its destructor.
Without exceptions, an app that calls exit() is never unloaded and the next app start waits the full timeout.
- netutils-module: ping, wget, ifconfig, nslookup, etc.
- Core2: support power off via software
- Improve Setup: Show "Step done" screen
Expand Down
2 changes: 1 addition & 1 deletion Modules/app-module/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ tactility_add_module(app-module

# 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 where those are wrapped: via
# -Wl,--wrap= on ESP-IDF (see the top-level CMakeLists.txt), or via source/stdio_wrap.cpp's own
# -Wl,--wrap= on ESP-IDF (see the top-level CMakeLists.txt), or via source/stdio_wrap_posix.cpp's own
# read/write/close and printf-family definitions on POSIX (dyld interpose on Apple, plain
# symbol overrides elsewhere - see that file for why POSIX doesn't use --wrap).
tactility_get_module_name(app-module MODULE_NAME)
Expand Down
9 changes: 9 additions & 0 deletions Modules/app-module/include/app/io.h
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,15 @@ int app_io_close(int fd);
*/
error_t app_io_await(int fd, AppFileWait wait, TickType_t timeout);

/**
* FD-table dispatch for AppFileOps::poll(): reports readiness without blocking.
* @param[out] out_bits APP_FILE_READABLE and/or APP_FILE_WRITABLE
* @retval ERROR_NOT_FOUND @a fd isn't an app fd of the calling task's own app instance
* @retval ERROR_INVALID_STATE @a fd is an app fd that was already closed
* @retval ERROR_NONE on success
*/
error_t app_io_poll(int fd, uint32_t* out_bits);

/**
* FD-table dispatch for AppFileOps::ioctl().
* @retval ERROR_NOT_FOUND @a fd isn't bound in the calling task's own app instance fd table
Expand Down
8 changes: 8 additions & 0 deletions Modules/app-module/include/app/scheduler.h
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,14 @@ extern "C" {
*/
AppInstanceId app_scheduler_current_app_id(void);

/**
* Ends the calling app instance as if its main() returned @a status, by ending its task in place.
* The calling frames are never resumed. Like std::exit(), automatic objects are not destroyed
* (the simulator's thread exit may still destroy them). Memory the app allocated is not freed.
* Returns only when not called from an app instance's own main task.
*/
void app_scheduler_exit_current(int32_t status);

#ifdef __cplusplus
}
#endif
57 changes: 57 additions & 0 deletions Modules/app-module/private/app/private/stdio_wrap.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once

#include <cstddef>
#include <cstdio>
#include <sys/ioctl.h>
#include <sys/poll.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <termios.h>

// Neither platform's headers declare TIOCGWINSZ/struct winsize (ESP-IDF's sys/ioctl.h has no
// terminal ioctls at all; the guard is only for POSIX, where <sys/ioctl.h> already provides them).
#ifndef TIOCGWINSZ
#define TIOCGWINSZ 0x5413
struct winsize {
unsigned short ws_row;
unsigned short ws_col;
unsigned short ws_xpixel;
unsigned short ws_ypixel;
};
#endif

using PollFunction = int (*)(struct pollfd*, nfds_t, int);

/** @return true (with *out filled) if this was a window-size query on an app-owned fd */
bool tryAppWindowSize(int fd, unsigned long request, void* arg, struct winsize* out);

/** @return false if the caller isn't an app instance and should fall through to the real getcwd() */
bool tryAppGetCwd(char* buf, size_t size, char** out, int* outErrno);

/** @return false if the caller isn't an app instance and should fall through to the real chdir() */
bool tryAppChdir(const char* path, int* outResult, int* outErrno);

enum class AppFdState {
Bound,
/** An app fd that was already closed. Fails with EBADF rather than reaching a real fd of the same number. */
Closed,
/** Not an app fd. Falls through to the real syscall. */
NotAppFd,
};

AppFdState getAppFdState(int fd);

/**
* The tryApp*() fd helpers below set *outResult (and errno on failure) when they return true.
* @return false if @a fd isn't an app fd and the caller should fall through to the real call
*/
bool tryAppFstat(int fd, struct stat* st, int* outResult);
bool tryAppTcgetattr(int fd, struct termios* t, int* outResult);
bool tryAppTcsetattr(int fd, int* outResult);

/**
* Closed app fds report POLLNVAL.
* @return false if no fd in @a fds is an app fd and the caller should fall through to @a realPoll
*/
bool tryAppPoll(struct pollfd* fds, nfds_t nfds, int timeout, PollFunction realPoll, int* outResult);
Loading
Loading