From 102ca67729521b619b53f58955bacb0d32e3294c Mon Sep 17 00:00:00 2001 From: Ken Van Hoeylandt Date: Sun, 1 Nov 2026 16:21:22 +0100 Subject: [PATCH 1/3] Remove GraphicsDemo --- Apps/GraphicsDemo/CMakeLists.txt | 13 ------------- 1 file changed, 13 deletions(-) delete mode 100644 Apps/GraphicsDemo/CMakeLists.txt diff --git a/Apps/GraphicsDemo/CMakeLists.txt b/Apps/GraphicsDemo/CMakeLists.txt deleted file mode 100644 index 6e45c21..0000000 --- a/Apps/GraphicsDemo/CMakeLists.txt +++ /dev/null @@ -1,13 +0,0 @@ -# tactility-cmakelists-version: 1 -cmake_minimum_required(VERSION 3.20) - -if (NOT DEFINED ENV{TACTILITY_SDK_PATH}) - message(FATAL_ERROR "TACTILITY_SDK_PATH environment variable is not set") -endif() - -get_filename_component(TACTILITY_SDK_PATH "$ENV{TACTILITY_SDK_PATH}" ABSOLUTE) -include("${TACTILITY_SDK_PATH}/TactilitySDK.cmake") - -tactility_project_pre(graphicsdemo) -project(graphicsdemo) -tactility_project_post(graphicsdemo) From 182b44b360c8c0a606afbfd149c4fe480ee920d5 Mon Sep 17 00:00:00 2001 From: Ken Van Hoeylandt Date: Sun, 1 Nov 2026 16:22:29 +0100 Subject: [PATCH 2/3] Update tt.py --- tt.py | 266 +++++++++++++++++++++++++++++++++++++--------------------- 1 file changed, 170 insertions(+), 96 deletions(-) diff --git a/tt.py b/tt.py index ca4fe6a..e009387 100755 --- a/tt.py +++ b/tt.py @@ -7,30 +7,35 @@ Both ~/.tactility and TACTILITY_SDK_PATH hold sdk//tools/ and sdk//platform-/TactilitySDK/. The tt (Linux, macOS) and tt.ps1 (Windows PowerShell) wrappers next to this script run it. The SDK version is read from ./manifest.properties, or else from the first Apps/*/manifest.properties or apps/*/manifest.properties. +Without a manifest, the SDK version that was chosen before is used (.tactility/sdk_version in the current folder), +else the version of the device that "tt host" stored, else it asks for one. +"tt host" and "tt sdk" are built in, because they need no SDK: they show or store the device host (.tactility/device_host) and the SDK version (.tactility/sdk_version). """ import hashlib -import io import json import os import re import shutil +import socket +import ssl import subprocess import sys import time import urllib.request import zipfile -TT_VERSION = "1.3.1" +TT_VERSION = "1.4.0" CDN_URL = os.environ.get("TT_CDN", "https://cdn.tactilityproject.org") HOME_DIRECTORY = os.path.join(os.path.expanduser("~"), ".tactility") INDEX_VALIDITY_SECONDS = 3600 +DEVICE_PORT = 6667 +DEVICE_HOST_PATH = os.path.join(".tactility", "device_host") +SDK_VERSION_PATH = os.path.join(".tactility", "sdk_version") +DEVICE_TIMEOUT_SECONDS = 5 +DEVICE_REPLY_MAX_BYTES = 1024 COMMAND_PATTERN = re.compile(r"^[a-z0-9-]+$") VERSION_PATTERN = re.compile(r"^[0-9A-Za-z.+-]+$") -ID_PATTERN = re.compile(r"^[0-9A-Za-z.]{5,32}$") -NAME_PATTERN = re.compile(r"^[0-9A-Za-z -]{2,32}$") -NEW_PROJECT_ARCHIVE = "new-project.zip" -PROJECT_VARIANTS = ("gui", "terminal", "module") def fail(message): @@ -88,16 +93,23 @@ def is_valid_version(version): return VERSION_PATTERN.match(version) is not None and ".." not in version +def read_line(): + """Reads from the input one byte at a time, so the command that tt starts still gets the rest of a piped input""" + line = bytearray() + while True: + byte = os.read(0, 1) + if not byte or byte == b"\n": + return line.decode("utf-8", errors="replace").strip() + line += byte + + def ask(question, default=None, is_valid=None, description="invalid value"): """Asks until the answer is valid. An empty answer or the end of the input gives the default. Returns None when there is none.""" suffix = f" [{default}]" if default is not None else "" while True: - try: - # On stderr, so the prompts don't end up in the output of the command - print(f"{question}{suffix}: ", end="", file=sys.stderr, flush=True) - answer = input().strip() - except EOFError: - answer = "" + # On stderr, so the prompts don't end up in the output of the command + print(f"{question}{suffix}: ", end="", file=sys.stderr, flush=True) + answer = read_line() if not answer: return default if is_valid is None or is_valid(answer): @@ -105,105 +117,157 @@ def ask(question, default=None, is_valid=None, description="invalid value"): print(f"tt: {description}", file=sys.stderr) -def ask_yes_no(question, default): - answer = ask(f"{question} [{'Y/n' if default else 'y/N'}]") - if answer is None: - return default - return answer.lower() in ("y", "yes") +def read_stored(path): + try: + with open(path) as file: + return file.read().strip() or None + except OSError: + return None + + +def write_stored(path, value): + os.makedirs(os.path.dirname(path), exist_ok=True) + with open(path, "w") as file: + file.write(f"{value}\n") -def read_new_project_archive(version): - """Returns the content of the new project archive of the SDK version, from the local SDK or else the CDN""" - local_path = os.environ.get("TACTILITY_SDK_PATH") or None - if local_path is not None: - archive_path = os.path.join(version_path(os.path.abspath(local_path), version), NEW_PROJECT_ARCHIVE) - if not os.path.isfile(archive_path): - fail(f"new project not found: {archive_path}") - with open(archive_path, "rb") as file: - return file.read() - version_directory = version_path(HOME_DIRECTORY, version) - os.makedirs(version_directory, exist_ok=True) - entry = load_index(version, version_directory).get("newProject") - if entry is None: - fail(f"SDK {version} has no new project") - archive_path = os.path.join(version_directory, NEW_PROJECT_ARCHIVE) - if not download(sdk_url(version, entry["file"]), archive_path): - fail(f"failed to download the new project of SDK {version}") +def read_device_host(): + return read_stored(DEVICE_HOST_PATH) + + +def read_bounded_line(connection, deadline): + """Returns the first line of the connection, or None when it has no newline within the size limit or before the deadline""" + data = bytearray() + while b"\n" not in data: + remaining = deadline - time.monotonic() + if remaining <= 0 or len(data) >= DEVICE_REPLY_MAX_BYTES: + return None + connection.settimeout(remaining) + chunk = connection.recv(DEVICE_REPLY_MAX_BYTES - len(data)) + if not chunk: + return None + data += chunk + return bytes(data.split(b"\n", 1)[0]) + + +def query_device_info(host): + """Returns the info of the device (vendor, name, id and version), or None when it can't be read""" + context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) + # The info is public and the device has a self-signed certificate + context.check_hostname = False + context.verify_mode = ssl.CERT_NONE + hello = {"protocol": 1, "mode": "info", "client": "tt"} + deadline = time.monotonic() + DEVICE_TIMEOUT_SECONDS try: - with open(archive_path, "rb") as file: - content = file.read() - finally: - os.remove(archive_path) - if hashlib.sha256(content).hexdigest() != entry["sha256"]: - fail(f"the new project of SDK {version} doesn't match its checksum") - return content - - -def generate_project(version, package_id, app_id, app_name, variant): - """Extracts the new project of the SDK version into . and fills in the answers""" - root = os.path.realpath(".") - created = [] - with zipfile.ZipFile(io.BytesIO(read_new_project_archive(version))) as archive: - for member in archive.infolist(): - if member.is_dir() or not member.filename.startswith(f"{variant}/"): - continue - relative_path = member.filename[len(variant) + 1:] - destination = os.path.realpath(os.path.join(root, relative_path)) - if not destination.startswith(root + os.sep): - fail(f"invalid entry in the new project: {member.filename}") - content = archive.read(member) - if relative_path == "manifest.properties": - text = content.decode("utf-8") - for placeholder, value in (("@SDK_VERSION@", version), ("@PACKAGE_ID@", package_id), ("@APP_ID@", app_id), ("@APP_NAME@", app_name)): - text = text.replace(placeholder, value) - content = text.encode("utf-8") - os.makedirs(os.path.dirname(destination), exist_ok=True) - with open(destination, "wb") as file: - file.write(content) - created.append(relative_path) - if "manifest.properties" not in created: - fail(f"the new project of SDK {version} has no {variant} variant") - return created - - -def offer_new_project(): - """Asks to generate a new app or module project. Returns the manifest path when it did, else None""" - if not ask_yes_no("Generate a new project in this folder?", False): + with socket.create_connection((host, DEVICE_PORT), DEVICE_TIMEOUT_SECONDS) as connection: + with context.wrap_socket(connection, server_hostname=host) as secure: + secure.sendall((json.dumps(hello, separators=(",", ":")) + "\n").encode("utf-8")) + line = read_bounded_line(secure, deadline) + if line is None: + print(f"tt: could not read the device info of {host}: no complete reply in time", file=sys.stderr) + return None + info = json.loads(line)["info"] + if not isinstance(info, dict) or not all(isinstance(info.get(key), str) for key in ("vendor", "name", "id", "version")) or not is_valid_version(info["version"]): + print(f"tt: could not read the device info of {host}: incomplete reply", file=sys.stderr) + return None + return info + except (OSError, ValueError, KeyError, TypeError) as error: + print(f"tt: could not read the device info of {host}: {error}", file=sys.stderr) return None - if os.path.exists("manifest.properties") or os.path.exists("main"): - fail("this folder already has a manifest.properties or main/") - version = ask("SDK version to use", None, is_valid_version, "invalid SDK version") - if version is None: - fail("no SDK version given") - variant = ask("Project type (gui, terminal or module)", "gui", lambda answer: answer in PROJECT_VARIANTS, "use gui, terminal or module") - package_id = ask("Package id", "example.helloworld", ID_PATTERN.match, "use 5 to 32 letters, digits and dots") - app_id = app_name = "" - if variant != "module": - app_id = ask("App id", "helloworld", ID_PATTERN.match, "use 5 to 32 letters, digits and dots") - app_name = ask("App name", "Hello World", NAME_PATTERN.match, "use 2 to 32 letters, digits, spaces and dashes") - for path in generate_project(version, package_id, app_id, app_name, variant): - print(f"Created {path}", file=sys.stderr) - return "manifest.properties" + + +def print_device_info(info): + name = f"{info['vendor']} {info['name']}" if info["vendor"] else info["name"] + print(f"tt: using device {name} ({info['id']}) with Tactility {info['version']}", file=sys.stderr) + + +def resolve_device_target(): + """Returns the SDK version of the stored device, or None""" + host = read_device_host() + if host is None: + return None + info = query_device_info(host) + if info is None: + return None + print_device_info(info) + return info["version"] + + +def host_command(arguments): + """Shows the stored device host, or stores one and checks that the device answers""" + if not arguments: + print(read_device_host() or "none") + elif len(arguments) == 1 and arguments[0].strip(): + host = arguments[0].strip() + write_stored(DEVICE_HOST_PATH, host) + print(f"Device host: {host}") + info = query_device_info(host) + if info is not None: + print_device_info(info) + else: + fail("usage: tt host [hostname]") + + +def read_project_properties(manifest_path): + """Returns the properties of the manifest and its SDK version. Fails when the version is missing or invalid.""" + manifest = read_properties(manifest_path) + version = manifest.get("target.sdk") + if not version: + fail(f"target.sdk not found in {manifest_path}") + if not is_valid_version(version): + fail(f"invalid target.sdk in {manifest_path}: {version}") + return manifest, version + + +def read_stored_sdk_version(): + """Returns the stored SDK version, or None when it is missing or invalid""" + version = read_stored(SDK_VERSION_PATH) + return version if version is not None and is_valid_version(version) else None + + +def sdk_command(arguments): + """Shows the SDK version with its source, or stores one. A project's SDK version still takes precedence.""" + if not arguments: + stored_version = read_stored_sdk_version() + manifest_path = find_manifest() + if manifest_path: + project_version = read_project_properties(manifest_path)[1] + print(f"{project_version} (from project {manifest_path})") + if stored_version: + print(f"{stored_version} in {SDK_VERSION_PATH} is not used while a project is found") + elif stored_version: + print(f"{stored_version} (from local setting {SDK_VERSION_PATH})") + else: + host = read_device_host() + info = query_device_info(host) if host else None + print(f"{info['version']} (from device {host})" if info else "none") + elif len(arguments) == 1 and is_valid_version(arguments[0]): + write_stored(SDK_VERSION_PATH, arguments[0]) + print(f"SDK version: {arguments[0]}") + else: + fail("usage: tt sdk [version]") def resolve_target(): """Returns the SDK version and platforms of the app manifest""" manifest_path = find_manifest() if manifest_path is None: + # A project always decides the SDK. Without one, a chosen version beats the device. + stored_version = read_stored_sdk_version() + if stored_version is not None: + print(f"tt: using SDK {stored_version} from {SDK_VERSION_PATH}", file=sys.stderr) + return stored_version, [] + device_version = resolve_device_target() + if device_version is not None: + return device_version, [] print("tt: no manifest.properties found in . or Apps/*/ or apps/*/", file=sys.stderr) - manifest_path = offer_new_project() - if manifest_path is None: - # Commands like "tt shell" don't need an app, only the tools of an SDK version + # Commands like "tt shell" and "tt app create" don't need an app, only the tools of an SDK version version = ask("SDK version to use without a project (leave empty to abort)", None, is_valid_version, "invalid SDK version") if version is None: fail("no manifest.properties found in . or Apps/*/ or apps/*/") + write_stored(SDK_VERSION_PATH, version) return version, [] - manifest = read_properties(manifest_path) - version = manifest.get("target.sdk") - if not version: - fail(f"target.sdk not found in {manifest_path}") - if not is_valid_version(version): - fail(f"invalid target.sdk in {manifest_path}: {version}") + manifest, version = read_project_properties(manifest_path) platforms = [platform.strip() for platform in manifest.get("target.platforms", "").split(",") if platform.strip()] return version, platforms @@ -357,6 +421,10 @@ def print_help(version, tools_directory): """Without an app manifest, the version and its commands are unknown""" print("Usage: tt [arguments]") print("") + print("Built-in commands:") + print(" host") + print(" sdk") + print("") if version is None: print("Run this in an app folder, or in a folder with Apps/*/ or apps/*/, to list the commands of its SDK") else: @@ -392,6 +460,12 @@ def run_tool(tool_path, arguments, environment): def main(arguments): command = arguments[0] if arguments else None + if command == "host": + host_command(arguments[1:]) + return + if command == "sdk": + sdk_command(arguments[1:]) + return is_help = command is None or command in ("help", "--help") if not is_help and not is_command(command): fail(f"command not found: {command}") From e69bf0f8bdc22152526ead0811b59eee1ba9e9fb Mon Sep 17 00:00:00 2001 From: Ken Van Hoeylandt Date: Sun, 1 Nov 2026 16:43:39 +0100 Subject: [PATCH 3/3] Bump all app versions, remove GraphicsDemo --- Apps/AirMouse/manifest.properties | 4 +- Apps/Brainfuck/manifest.properties | 4 +- Apps/Breakout/manifest.properties | 4 +- Apps/BusyBoxVi/manifest.properties | 4 +- Apps/Calculator/manifest.properties | 4 +- Apps/Diceware/manifest.properties | 4 +- Apps/Doom/manifest.properties | 4 +- Apps/EpubReader/manifest.properties | 4 +- Apps/EspNowBridge/manifest.properties | 4 +- Apps/GPIO/manifest.properties | 4 +- Apps/GfxBenchmark/manifest.properties | 4 +- Apps/GraphicsDemo/main/CMakeLists.txt | 11 -- Apps/GraphicsDemo/main/Include/Application.h | 6 - Apps/GraphicsDemo/main/Source/Application.cpp | 125 ----------------- Apps/GraphicsDemo/main/Source/Main.cpp | 128 ------------------ Apps/GraphicsDemo/manifest.properties | 10 -- Apps/HelloWorld/manifest.properties | 4 +- Apps/M5UnitTest/manifest.properties | 4 +- Apps/Magic8Ball/manifest.properties | 4 +- Apps/MediaKeys/manifest.properties | 4 +- Apps/MidiController/manifest.properties | 4 +- Apps/MystifyDemo/manifest.properties | 4 +- Apps/MystifySpiDemo/manifest.properties | 4 +- Apps/Neatvi/manifest.properties | 4 +- Apps/Rcc700/manifest.properties | 4 +- Apps/SatelliteTracker/manifest.properties | 4 +- Apps/SerialConsole/manifest.properties | 4 +- Apps/Snake/manifest.properties | 4 +- Apps/TamaTac/manifest.properties | 4 +- Apps/TodoList/manifest.properties | 4 +- Apps/Tone/manifest.properties | 4 +- Apps/TwoEleven/manifest.properties | 4 +- Apps/UnitLcdGfxDemo/manifest.properties | 4 +- Apps/WebServer/manifest.properties | 4 +- Apps/Xcc700/manifest.properties | 4 +- 35 files changed, 60 insertions(+), 340 deletions(-) delete mode 100644 Apps/GraphicsDemo/main/CMakeLists.txt delete mode 100644 Apps/GraphicsDemo/main/Include/Application.h delete mode 100644 Apps/GraphicsDemo/main/Source/Application.cpp delete mode 100644 Apps/GraphicsDemo/main/Source/Main.cpp delete mode 100644 Apps/GraphicsDemo/manifest.properties diff --git a/Apps/AirMouse/manifest.properties b/Apps/AirMouse/manifest.properties index 36b04ac..650f800 100644 --- a/Apps/AirMouse/manifest.properties +++ b/Apps/AirMouse/manifest.properties @@ -2,8 +2,8 @@ manifest.version=0.3 target.sdk=0.8.0-dev target.platforms=esp32s3,esp32p4 id=tactility.airmouse -version.name=0.4.0 -version.code=4 +version.name=0.5.0 +version.code=5 name=Air Mouse description=Bluetooth air mouse. Orientation-aware tilt control with auto calibration, adjustable sensitivity, a settings screen, on-screen left/right click buttons, and an optional auto-clutch that freezes the pointer while the device is being repositioned. app.0.id=tactility.airmouse diff --git a/Apps/Brainfuck/manifest.properties b/Apps/Brainfuck/manifest.properties index b8a82be..9a74954 100644 --- a/Apps/Brainfuck/manifest.properties +++ b/Apps/Brainfuck/manifest.properties @@ -2,8 +2,8 @@ manifest.version=0.3 target.sdk=0.8.0-dev target.platforms=esp32,esp32s3,esp32c6,esp32p4 id=tactility.brainfuck -version.name=0.18.0 -version.code=18 +version.name=0.19.0 +version.code=19 name=Brainfuck description=Brainfuck esoteric language interpreter app.0.id=tactility.brainfuck diff --git a/Apps/Breakout/manifest.properties b/Apps/Breakout/manifest.properties index bd129aa..7d65a99 100644 --- a/Apps/Breakout/manifest.properties +++ b/Apps/Breakout/manifest.properties @@ -2,8 +2,8 @@ manifest.version=0.3 target.sdk=0.8.0-dev target.platforms=esp32,esp32s3,esp32c6,esp32p4 id=tactility.breakout -version.name=0.22.0 -version.code=22 +version.name=0.23.0 +version.code=23 name=Breakout description=Classic brick-breaking arcade game app.0.id=tactility.breakout diff --git a/Apps/BusyBoxVi/manifest.properties b/Apps/BusyBoxVi/manifest.properties index 190a9f2..828dccd 100644 --- a/Apps/BusyBoxVi/manifest.properties +++ b/Apps/BusyBoxVi/manifest.properties @@ -2,8 +2,8 @@ manifest.version=0.3 target.sdk=0.8.0-dev target.platforms=all id=tactility.busyboxvi -version.name=0.4.0 -version.code=4 +version.name=0.5.0 +version.code=5 name=BusyBox vi description=BusyBox vi text editor app.0.id=tactility.busyboxvi diff --git a/Apps/Calculator/manifest.properties b/Apps/Calculator/manifest.properties index 03c1cfd..1b3868e 100644 --- a/Apps/Calculator/manifest.properties +++ b/Apps/Calculator/manifest.properties @@ -2,8 +2,8 @@ manifest.version=0.3 target.sdk=0.8.0-dev target.platforms=esp32,esp32s3,esp32c6,esp32p4 id=tactility.calculator -version.name=0.19.0 -version.code=19 +version.name=0.20.0 +version.code=20 name=Calculator app.0.id=tactility.calculator app.0.name=Calculator diff --git a/Apps/Diceware/manifest.properties b/Apps/Diceware/manifest.properties index 2d05f6e..03c7a29 100644 --- a/Apps/Diceware/manifest.properties +++ b/Apps/Diceware/manifest.properties @@ -2,8 +2,8 @@ manifest.version=0.3 target.sdk=0.8.0-dev target.platforms=esp32,esp32s3,esp32c6,esp32p4 id=tactility.diceware -version.name=0.21.0 -version.code=21 +version.name=0.22.0 +version.code=22 name=Diceware description=Password generator app.0.id=tactility.diceware diff --git a/Apps/Doom/manifest.properties b/Apps/Doom/manifest.properties index 5131bb0..3300bb6 100644 --- a/Apps/Doom/manifest.properties +++ b/Apps/Doom/manifest.properties @@ -2,8 +2,8 @@ manifest.version=0.3 target.sdk=0.8.0-dev target.platforms=esp32p4 id=tactility.doom -version.name=0.13.0 -version.code=13 +version.name=0.14.0 +version.code=14 name=Doom description=Classic DOOM. Tab5 only, keyboard required to play. app.0.id=tactility.doom diff --git a/Apps/EpubReader/manifest.properties b/Apps/EpubReader/manifest.properties index d3bf9f6..fcbd751 100644 --- a/Apps/EpubReader/manifest.properties +++ b/Apps/EpubReader/manifest.properties @@ -2,8 +2,8 @@ manifest.version=0.3 target.sdk=0.8.0-dev target.platforms=esp32s3,esp32p4 id=tactility.epubreader -version.name=0.18.0 -version.code=18 +version.name=0.19.0 +version.code=19 requires.ram=2M name=ePub Reader description=ePub and text file reader. diff --git a/Apps/EspNowBridge/manifest.properties b/Apps/EspNowBridge/manifest.properties index 6d8144f..f1551d0 100644 --- a/Apps/EspNowBridge/manifest.properties +++ b/Apps/EspNowBridge/manifest.properties @@ -2,8 +2,8 @@ manifest.version=0.3 target.sdk=0.8.0-dev target.platforms=esp32p4 id=tactility.espnowbridge -version.name=0.16.0 -version.code=16 +version.name=0.17.0 +version.code=17 name=ESP-NOW Bridge description=Companion app for updating P4 device C6 co-processor firmware to enable ESP-NOW bridge support. app.0.id=tactility.espnowbridge diff --git a/Apps/GPIO/manifest.properties b/Apps/GPIO/manifest.properties index 2c95c13..c1fbf60 100644 --- a/Apps/GPIO/manifest.properties +++ b/Apps/GPIO/manifest.properties @@ -2,8 +2,8 @@ manifest.version=0.3 target.sdk=0.8.0-dev target.platforms=esp32,esp32s3,esp32c6,esp32p4 id=tactility.gpio -version.name=0.21.0 -version.code=21 +version.name=0.22.0 +version.code=22 name=GPIO description=Shows the live state of all GPIO pins app.0.id=tactility.gpio diff --git a/Apps/GfxBenchmark/manifest.properties b/Apps/GfxBenchmark/manifest.properties index fd0277a..6ba51e3 100644 --- a/Apps/GfxBenchmark/manifest.properties +++ b/Apps/GfxBenchmark/manifest.properties @@ -2,8 +2,8 @@ manifest.version=0.3 target.sdk=0.8.0-dev target.platforms=esp32,esp32s3,esp32c6,esp32p4 id=tactility.gfxbenchmark -version.name=0.1.0 -version.code=1 +version.name=0.2.0 +version.code=2 name=Gfx Benchmark description=PDQ-style primitive benchmark for the direct-to-display Gfx library, with an on-screen results table. app.0.id=tactility.gfxbenchmark diff --git a/Apps/GraphicsDemo/main/CMakeLists.txt b/Apps/GraphicsDemo/main/CMakeLists.txt deleted file mode 100644 index d64a8cc..0000000 --- a/Apps/GraphicsDemo/main/CMakeLists.txt +++ /dev/null @@ -1,11 +0,0 @@ -include("$ENV{TACTILITY_SDK_PATH}/TactilitySDK.cmake") - -file(GLOB_RECURSE SOURCE_FILES Source/*.c*) -file(GLOB_RECURSE GFX_FILES ../../../Libraries/Gfx/Source/*.c*) -file(GLOB_RECURSE INPUT_FILES ../../../Libraries/Input/Source/*.c*) - -tactility_component_register( - SRCS ${SOURCE_FILES} ${GFX_FILES} ${INPUT_FILES} - INCLUDE_DIRS "Include" ../../../Libraries/Gfx/Include ../../../Libraries/Input/Include - REQUIRES graphics-module esp_driver_ppa -) diff --git a/Apps/GraphicsDemo/main/Include/Application.h b/Apps/GraphicsDemo/main/Include/Application.h deleted file mode 100644 index a84e34c..0000000 --- a/Apps/GraphicsDemo/main/Include/Application.h +++ /dev/null @@ -1,6 +0,0 @@ -#pragma once - -#include -#include - -void runApplication(gfx::Target* target, AnyInput* input); diff --git a/Apps/GraphicsDemo/main/Source/Application.cpp b/Apps/GraphicsDemo/main/Source/Application.cpp deleted file mode 100644 index 56ca18f..0000000 --- a/Apps/GraphicsDemo/main/Source/Application.cpp +++ /dev/null @@ -1,125 +0,0 @@ -#include "Application.h" - -#include - -#include -#include - -#include - -#include -#include - -constexpr auto TAG = "Application"; - -static uint16_t toRgb565(uint8_t r, uint8_t g, uint8_t b) { - return static_cast(((r & 0xF8) << 8) | ((g & 0xFC) << 3) | (b >> 3)); -} - -/** Three colour bands across the top half. */ -static void drawBandsArea(gfx::Surface& surface, int y, int height) { - const int width = surface.width(); - const int stripe = width / 3; - if (stripe <= 0) { - return; - } - - // On a grayscale panel, equal-luminance colours (pure red/green/blue all average to 85) - // collapse into one flat block. Use explicit dark/mid/light greys there so the three bands stay - // visually distinct; colour displays still get real RGB. - const bool grayscale = surface.format() == DISPLAY_COLOR_FORMAT_GRAYSCALE8; - const uint16_t first = grayscale ? toRgb565(64, 64, 64) : toRgb565(255, 0, 0); - const uint16_t second = grayscale ? toRgb565(160, 160, 160) : toRgb565(0, 255, 0); - const uint16_t third = grayscale ? toRgb565(230, 230, 230) : toRgb565(0, 0, 255); - - surface.fillRect(0, y, stripe, height, first); - surface.fillRect(stripe, y, stripe, height, second); - surface.fillRect(stripe * 2, y, width - stripe * 2, height, third); -} - -/** Red/green/blue gradients across the bottom half. */ -static void drawFadingArea(gfx::Surface& surface, int y, int height) { - const int width = surface.width(); - const int stroke = std::max(width / 3, 1); - - for (int x = 0; x < width; x++) { - uint8_t r = 0; - uint8_t g = 0; - uint8_t b = 0; - if (x < stroke) { - r = static_cast(x * 255 / stroke); - } else if (x < stroke * 2) { - g = static_cast((x - stroke) * 255 / stroke); - } else { - b = static_cast((x - stroke * 2) * 255 / stroke); - } - surface.fillRect(x, y, 1, height, toRgb565(r, g, b)); - } -} - -static void drawPattern(gfx::Surface& surface, int display_height) { - // Clearing the surface also makes the callback overwrite every pixel it covers, so the target - // doesn't have to copy the previous frame forward (Prepare::Overwrite). - surface.clear(); - - const int half = display_height / 2; - drawBandsArea(surface, 0, half); - drawFadingArea(surface, half, display_height - half); -} - -void runApplication(gfx::Target* target, AnyInput* input) { - const gfx::Info& info = target->info(); - const bool slow_refresh = info.slowRefresh; - - struct TaskEventGroup event_group {}; - task_event_group_construct(&event_group); - struct AppEventSubscription sub {}; - check(app_event_subscribe(&sub, &event_group) == ERROR_NONE); - - bool should_close = false; - do { - target->render([height = info.height](gfx::Surface& surface) { - drawPattern(surface, height); - }, gfx::Prepare::Overwrite); - - if (slow_refresh) { - // Fast partial updates never fully resolve grayscale on this kind of panel - they - // flatten it towards black and white. One quality pass does. - target->refresh(); - } - - struct AppEvent event {}; - while (app_event_poll(&sub, &event) == ERROR_NONE) { - if (event.type == APP_EVENT_CLOSE) { - should_close = true; - } - } - if (should_close) { - break; - } - - if (slow_refresh) { - // The content is static, and repeating it continuously would leave a slow panel - // flickering through partial and quality refreshes. Draw once and wait for input. - while (true) { - struct AppEvent pending {}; - while (app_event_poll(&sub, &pending) == ERROR_NONE) { - if (pending.type == APP_EVENT_CLOSE) { - should_close = true; - } - } - if (should_close || input->activity()) { - break; - } - tt::kernel::delayTicks(tt::kernel::millisToTicks(50)); - } - break; - } - - // Give other tasks space to breathe: SPI displays would otherwise time out SPI SD access. - tt::kernel::delayTicks(1); - } while (!input->activity()); - - check(app_event_unsubscribe(&sub) == ERROR_NONE); - task_event_group_destruct(&event_group); -} diff --git a/Apps/GraphicsDemo/main/Source/Main.cpp b/Apps/GraphicsDemo/main/Source/Main.cpp deleted file mode 100644 index 9aca1b2..0000000 --- a/Apps/GraphicsDemo/main/Source/Main.cpp +++ /dev/null @@ -1,128 +0,0 @@ -#include "Application.h" - -#include - -#include -#include -#include -#include - -#include -#include - -#include -#include -#include -#include -#include -#include - -constexpr auto TAG = "Main"; - -// Shows a blocking error dialog and waits for it to close (so the user actually gets to read it) -// before the caller finishes this app - this app never creates a window of its own, so there's -// nothing else keeping it around for the dialog to be seen against. -static void showErrorAndWait(AppInstanceId appInstanceId, const char* message) { - const char* argv[] = { "Error", message, "OK" }; - uint32_t dialogInstanceId = 0; - app_start_for_result("tactility.alertdialog", 3, argv, appInstanceId, &dialogInstanceId); - if (dialogInstanceId == 0) { - return; - } - - struct TaskEventGroup event_group {}; - task_event_group_construct(&event_group); - - struct AppEventSubscription sub {}; - check(app_event_subscribe(&sub, &event_group) == ERROR_NONE); - - while (true) { - task_event_group_wait_any(&event_group, nullptr, portMAX_DELAY); - - bool done = false; - struct AppEvent event {}; - while (app_event_poll(&sub, &event) == ERROR_NONE) { - if (event.type == APP_EVENT_CLOSE) { - done = true; - break; - } - if (event.type == APP_EVENT_RESULT && event.result.launch_id == dialogInstanceId) { - app_manager_stop(dialogInstanceId); - done = true; - break; - } - } - if (done) break; - } - - check(app_event_unsubscribe(&sub) == ERROR_NONE); - task_event_group_destruct(&event_group); -} - -extern "C" { - -int main(int argc, char* argv[]) { - AppInstanceId app_instance_id = app_scheduler_current_app_id(); - - struct Device* display_device; - if (device_get_first_active_by_type(&DISPLAY_TYPE, &display_device) != ERROR_NONE) { - ESP_LOGE(TAG, "No display device found"); - showErrorAndWait(app_instance_id, "No display device was found."); - return 0; - } - - // Stop LVGL first (because it's currently using the drivers we want to use) - module_stop(&lvgl_module); - - ESP_LOGI(TAG, "Creating display target"); - auto target = new gfx::Target(display_device); - device_put(display_device); - - if (!target->valid()) { - ESP_LOGE(TAG, "Display doesn't support direct rendering"); - delete target; - - if (!module_is_started(&lvgl_module)) { - module_start(&lvgl_module); - } - - showErrorAndWait(app_instance_id, "This display can't be used for direct rendering."); - return 0; - } - - auto input = new AnyInput(); - if (!input->available()) { - ESP_LOGE(TAG, "No input device found"); - delete input; - delete target; - - if (!module_is_started(&lvgl_module)) { - module_start(&lvgl_module); - } - - showErrorAndWait(app_instance_id, "No input device was found."); - return 0; - } - - // Run the main logic - ESP_LOGI(TAG, "Running application"); - runApplication(target, input); - - ESP_LOGI(TAG, "Cleanup display target"); - delete target; - - ESP_LOGI(TAG, "Cleanup input"); - delete input; - - // Restart LVGL to resume rendering of regular apps - if (!module_is_started(&lvgl_module)) { - ESP_LOGI(TAG, "Restarting LVGL"); - module_start(&lvgl_module); - } - - ESP_LOGI(TAG, "Stopping application"); - - return 0; -} - -} diff --git a/Apps/GraphicsDemo/manifest.properties b/Apps/GraphicsDemo/manifest.properties deleted file mode 100644 index 16c8bd1..0000000 --- a/Apps/GraphicsDemo/manifest.properties +++ /dev/null @@ -1,10 +0,0 @@ -manifest.version=0.3 -target.sdk=0.8.0-dev -target.platforms=esp32,esp32s3,esp32c6,esp32p4 -id=tactility.graphicsdemo -version.name=0.20.0 -version.code=20 -name=Graphics Demo -app.0.id=tactility.graphicsdemo -app.0.name=Graphics Demo -app.0.binary=graphicsdemo diff --git a/Apps/HelloWorld/manifest.properties b/Apps/HelloWorld/manifest.properties index 549584f..c6f4044 100644 --- a/Apps/HelloWorld/manifest.properties +++ b/Apps/HelloWorld/manifest.properties @@ -2,8 +2,8 @@ manifest.version=0.3 target.sdk=0.8.0-dev target.platforms=all id=tactility.helloworld -version.name=0.18.0 -version.code=18 +version.name=0.19.0 +version.code=19 name=Hello World app.0.id=tactility.helloworld app.0.name=Hello World diff --git a/Apps/M5UnitTest/manifest.properties b/Apps/M5UnitTest/manifest.properties index 709d399..1b9e662 100644 --- a/Apps/M5UnitTest/manifest.properties +++ b/Apps/M5UnitTest/manifest.properties @@ -2,8 +2,8 @@ manifest.version=0.3 target.sdk=0.8.0-dev target.platforms=esp32s3,esp32p4 id=tactility.m5unittest -version.name=0.17.0 -version.code=17 +version.name=0.18.0 +version.code=18 name=M5 Unit Test app.0.id=tactility.m5unittest app.0.name=M5 Unit Test diff --git a/Apps/Magic8Ball/manifest.properties b/Apps/Magic8Ball/manifest.properties index d555c33..ff35691 100644 --- a/Apps/Magic8Ball/manifest.properties +++ b/Apps/Magic8Ball/manifest.properties @@ -2,8 +2,8 @@ manifest.version=0.3 target.sdk=0.8.0-dev target.platforms=esp32,esp32s3,esp32c6,esp32p4 id=tactility.magic8ball -version.name=0.18.0 -version.code=18 +version.name=0.19.0 +version.code=19 name=Magic 8-Ball app.0.id=tactility.magic8ball app.0.name=Magic 8-Ball diff --git a/Apps/MediaKeys/manifest.properties b/Apps/MediaKeys/manifest.properties index 556f669..486a501 100644 --- a/Apps/MediaKeys/manifest.properties +++ b/Apps/MediaKeys/manifest.properties @@ -2,8 +2,8 @@ manifest.version=0.3 target.sdk=0.8.0-dev target.platforms=esp32s3,esp32p4 id=tactility.mediakeys -version.name=0.21.0 -version.code=21 +version.name=0.22.0 +version.code=22 name=Media Keys description=Bluetooth media keys. Touch or Physical Keyboard control\nB - previous, P - play/pause, N - next, M - mute, D - volume down, U - volume up.\nQ or ESC to exit focus. app.0.id=tactility.mediakeys diff --git a/Apps/MidiController/manifest.properties b/Apps/MidiController/manifest.properties index 8d202ed..531df70 100644 --- a/Apps/MidiController/manifest.properties +++ b/Apps/MidiController/manifest.properties @@ -2,8 +2,8 @@ manifest.version=0.3 target.sdk=0.8.0-dev target.platforms=esp32p4 id=tactility.midicontroller -version.name=0.5.0 -version.code=5 +version.name=0.6.0 +version.code=6 name=Midi Controller description=Midi Controller app.0.id=tactility.midicontroller diff --git a/Apps/MystifyDemo/manifest.properties b/Apps/MystifyDemo/manifest.properties index 57ee46f..ac4f28b 100644 --- a/Apps/MystifyDemo/manifest.properties +++ b/Apps/MystifyDemo/manifest.properties @@ -2,8 +2,8 @@ manifest.version=0.3 target.sdk=0.8.0-dev target.platforms=esp32,esp32s3,esp32c6,esp32p4 id=tactility.mystifydemo -version.name=0.21.0 -version.code=21 +version.name=0.22.0 +version.code=22 name=Mystify Demo description=A graphics demo that is inspired by a Windows 95 screensaver. app.0.id=tactility.mystifydemo diff --git a/Apps/MystifySpiDemo/manifest.properties b/Apps/MystifySpiDemo/manifest.properties index 26d143b..d10c64a 100644 --- a/Apps/MystifySpiDemo/manifest.properties +++ b/Apps/MystifySpiDemo/manifest.properties @@ -2,8 +2,8 @@ manifest.version=0.3 target.sdk=0.8.0-dev target.platforms=esp32p4 id=tactility.mystifyspidemo -version.name=0.6.0 -version.code=6 +version.name=0.7.0 +version.code=7 name=Mystify SPI Demo description=Adds a 2.8" ILI9341 SPI panel as a second display via an app-packaged driver module: Mystify demo, live settings, dashboard and terminal CLI. app.0.id=tactility.mystifyspidemo diff --git a/Apps/Neatvi/manifest.properties b/Apps/Neatvi/manifest.properties index 4350f67..6660ac0 100644 --- a/Apps/Neatvi/manifest.properties +++ b/Apps/Neatvi/manifest.properties @@ -2,8 +2,8 @@ manifest.version=0.3 target.sdk=0.8.0-dev target.platforms=all id=tactility.neatvi -version.name=0.5.0 -version.code=5 +version.name=0.6.0 +version.code=6 requires.ram=2M name=Neatvi description=Neatvi text editor diff --git a/Apps/Rcc700/manifest.properties b/Apps/Rcc700/manifest.properties index 84cf1d3..c552510 100644 --- a/Apps/Rcc700/manifest.properties +++ b/Apps/Rcc700/manifest.properties @@ -2,8 +2,8 @@ manifest.version=0.3 target.sdk=0.8.0-dev target.platforms=all id=tactility.rcc700 -version.name=0.4.0 -version.code=4 +version.name=0.5.0 +version.code=5 requires.ram=2M name=rcc700 description=Mini C compiler for ESP32 RISC-V diff --git a/Apps/SatelliteTracker/manifest.properties b/Apps/SatelliteTracker/manifest.properties index 0d17fc4..61d5f4a 100644 --- a/Apps/SatelliteTracker/manifest.properties +++ b/Apps/SatelliteTracker/manifest.properties @@ -2,8 +2,8 @@ manifest.version=0.3 target.sdk=0.8.0-dev target.platforms=all id=tactility.satellitetracker -version.name=0.3.0 -version.code=3 +version.name=0.4.0 +version.code=4 name=Satellite Tracker description=World map with live satellite positions and passes app.0.id=tactility.satellitetracker diff --git a/Apps/SerialConsole/manifest.properties b/Apps/SerialConsole/manifest.properties index e542053..38c32ab 100644 --- a/Apps/SerialConsole/manifest.properties +++ b/Apps/SerialConsole/manifest.properties @@ -2,8 +2,8 @@ manifest.version=0.3 target.sdk=0.8.0-dev target.platforms=esp32,esp32s3,esp32c6,esp32p4 id=tactility.serialconsole -version.name=0.21.0 -version.code=21 +version.name=0.22.0 +version.code=22 name=Serial Console app.0.id=tactility.serialconsole app.0.name=Serial Console diff --git a/Apps/Snake/manifest.properties b/Apps/Snake/manifest.properties index f3b1b9f..00711a0 100644 --- a/Apps/Snake/manifest.properties +++ b/Apps/Snake/manifest.properties @@ -2,8 +2,8 @@ manifest.version=0.3 target.sdk=0.8.0-dev target.platforms=esp32,esp32s3,esp32c6,esp32p4 id=tactility.snake -version.name=0.22.0 -version.code=22 +version.name=0.23.0 +version.code=23 name=Snake description=Classic Snake game app.0.id=tactility.snake diff --git a/Apps/TamaTac/manifest.properties b/Apps/TamaTac/manifest.properties index 2c88015..620119f 100644 --- a/Apps/TamaTac/manifest.properties +++ b/Apps/TamaTac/manifest.properties @@ -2,8 +2,8 @@ manifest.version=0.3 target.sdk=0.8.0-dev target.platforms=esp32,esp32s3,esp32c6,esp32p4 id=tactility.tamatac -version.name=0.17.0 -version.code=17 +version.name=0.18.0 +version.code=18 requires.ram=2M name=TamaTac description=Virtual pet inspired by Tamagotchi. diff --git a/Apps/TodoList/manifest.properties b/Apps/TodoList/manifest.properties index da38784..d9b5944 100644 --- a/Apps/TodoList/manifest.properties +++ b/Apps/TodoList/manifest.properties @@ -2,8 +2,8 @@ manifest.version=0.3 target.sdk=0.8.0-dev target.platforms=esp32,esp32s3,esp32c6,esp32p4 id=tactility.todolist -version.name=0.20.0 -version.code=20 +version.name=0.21.0 +version.code=21 name=Todo List description=Simple task list manager app.0.id=tactility.todolist diff --git a/Apps/Tone/manifest.properties b/Apps/Tone/manifest.properties index 21a298a..9a9c187 100644 --- a/Apps/Tone/manifest.properties +++ b/Apps/Tone/manifest.properties @@ -2,8 +2,8 @@ manifest.version=0.3 target.sdk=0.8.0-dev target.platforms=all id=tactility.tone -version.name=0.3.0 -version.code=3 +version.name=0.4.0 +version.code=4 name=Tone description=Tone generator and frequency sweeper app.0.id=tactility.tone diff --git a/Apps/TwoEleven/manifest.properties b/Apps/TwoEleven/manifest.properties index 3b88a8b..1d78d92 100644 --- a/Apps/TwoEleven/manifest.properties +++ b/Apps/TwoEleven/manifest.properties @@ -2,8 +2,8 @@ manifest.version=0.3 target.sdk=0.8.0-dev target.platforms=esp32,esp32s3,esp32c6,esp32p4 id=tactility.twoeleven -version.name=0.21.0 -version.code=21 +version.name=0.22.0 +version.code=22 name=2048 description=A fun, customizable 2048 sliding tile game for tactility!\nSlide tiles to combine numbers and reach 2048.\nChoose grid sizes: 3x3 (easy), 4x4 (classic), 5x5, or 6x6 (expert). app.0.id=tactility.twoeleven diff --git a/Apps/UnitLcdGfxDemo/manifest.properties b/Apps/UnitLcdGfxDemo/manifest.properties index 054224e..ce3d192 100644 --- a/Apps/UnitLcdGfxDemo/manifest.properties +++ b/Apps/UnitLcdGfxDemo/manifest.properties @@ -2,8 +2,8 @@ manifest.version=0.3 target.sdk=0.8.0-dev target.platforms=esp32s3,esp32p4 id=tactility.unitlcdgfxdemo -version.name=0.1.0 -version.code=1 +version.name=0.2.0 +version.code=2 name=UnitLCD Gfx Demo description=Drives an M5Stack LCD Unit through the Gfx SinkTarget/UnitLcdSink path while the primary display shows status. app.0.id=tactility.unitlcdgfxdemo diff --git a/Apps/WebServer/manifest.properties b/Apps/WebServer/manifest.properties index 38eaca0..9dfd098 100644 --- a/Apps/WebServer/manifest.properties +++ b/Apps/WebServer/manifest.properties @@ -2,8 +2,8 @@ manifest.version=0.3 target.sdk=0.8.0-dev target.platforms=esp32,esp32s3,esp32c6,esp32p4 id=tactility.webserver -version.name=0.3.0 -version.code=3 +version.name=0.4.0 +version.code=4 name=Web Server description=Web dashboard with a file browser, app manager and system information app.0.id=tactility.webserver diff --git a/Apps/Xcc700/manifest.properties b/Apps/Xcc700/manifest.properties index ab07104..0e1b295 100644 --- a/Apps/Xcc700/manifest.properties +++ b/Apps/Xcc700/manifest.properties @@ -2,8 +2,8 @@ manifest.version=0.3 target.sdk=0.8.0-dev target.platforms=all id=tactility.xcc700 -version.name=0.4.0 -version.code=4 +version.name=0.5.0 +version.code=5 name=xcc700 description=Mini C compiler for ESP32 Xtensa app.0.id=tactility.xcc700