diff --git a/.github/workflows/build-simulator.yml b/.github/workflows/build-simulator.yml index c9f6ac80..b781524b 100644 --- a/.github/workflows/build-simulator.yml +++ b/.github/workflows/build-simulator.yml @@ -11,6 +11,8 @@ jobs: runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v4 + with: + persist-credentials: false - name: "Build" uses: ./.github/actions/build-simulator with: @@ -21,6 +23,8 @@ jobs: runs-on: macos-latest steps: - uses: actions/checkout@v4 + with: + persist-credentials: false - name: "Build" uses: ./.github/actions/build-simulator with: diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b832709a..3390cb8d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -23,6 +23,8 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 + with: + persist-credentials: false - name: "Build SDK" uses: ./.github/actions/build-sdk with: @@ -66,7 +68,6 @@ jobs: { id: m5stack-cores3, arch: esp32s3 }, { id: m5stack-papers3, arch: esp32s3 }, { id: m5stack-stackchan, arch: esp32s3 }, - { id: m5stack-stickc-plus, arch: esp32 }, { id: m5stack-stickc-plus2, arch: esp32 }, { id: m5stack-sticks3, arch: esp32s3 }, { id: m5stack-tab5, arch: esp32p4 }, @@ -82,6 +83,8 @@ jobs: needs: [ BuildSdk ] steps: - uses: actions/checkout@v4 + with: + persist-credentials: false - name: "Build Firmware" uses: ./.github/actions/build-firmware with: diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 96631892..8a53dd96 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -31,8 +31,9 @@ jobs: runs-on: ubuntu-latest steps: - name: "Checkout repo" - uses: actions/checkout@v4 + - uses: actions/checkout@v4 with: + persist-credentials: false submodules: recursive - name: "Install Python Dependencies" shell: bash diff --git a/Buildscripts/CDN/generate-firmware-files.py b/Buildscripts/CDN/generate-firmware-files.py index 23fd6126..c3f99483 100644 --- a/Buildscripts/CDN/generate-firmware-files.py +++ b/Buildscripts/CDN/generate-firmware-files.py @@ -2,11 +2,9 @@ import subprocess from datetime import datetime, UTC import os import sys -import configparser from dataclasses import dataclass, asdict import json import shutil -from configparser import RawConfigParser VERBOSE = False DEVICES_FOLDER = "Devices" @@ -68,32 +66,29 @@ def exit_with_error(message): sys.exit(1) def read_properties_file(path): - config = configparser.RawConfigParser() - # Don't convert keys to lowercase - config.optionxform = str - config.read(path) - return config + properties = {} + with open(path, "r") as file: + for line in file: + stripped = line.strip() + if not stripped or stripped.startswith("#"): + continue + key, sep, value = line.partition("=") + if not sep: + continue + properties[key.strip()] = value.strip() + return properties -def get_property_or_none(properties: RawConfigParser, group: str, key: str): - if group not in properties.sections(): - return None - if key not in properties[group].keys(): - return None - return properties[group][key] +def get_property_or_none(properties: dict, group: str, key: str): + return properties.get(f"{group}.{key}") -def get_boolean_property_or_false(properties: RawConfigParser, group: str, key: str): - if group not in properties.sections(): - return False - if key not in properties[group].keys(): - return False - return properties[group][key] == "true" +def get_boolean_property_or_false(properties: dict, group: str, key: str): + return properties.get(f"{group}.{key}") == "true" -def get_property_or_exit(properties: RawConfigParser, group: str, key: str): - if group not in properties.sections(): - exit_with_error(f"Device properties does not contain group: {group}") - if key not in properties[group].keys(): - exit_with_error(f"Device properties does not contain key: {key}") - return properties[group][key] +def get_property_or_exit(properties: dict, group: str, key: str): + full_key = f"{group}.{key}" + if full_key not in properties: + exit_with_error(f"Device properties does not contain key: {full_key}") + return properties[full_key] def read_device_properties(device_id): mapping_file_path = os.path.join(DEVICES_FOLDER, device_id, "device.properties") @@ -129,7 +124,7 @@ def to_manifest_chip_name(name): return "" -def process_device(in_path: str, out_path: str, device_directory: str, device_id: str, device_properties: RawConfigParser, version: str): +def process_device(in_path: str, out_path: str, device_directory: str, device_id: str, device_properties: dict, version: str): in_device_path = os.path.join(in_path, device_directory) in_device_binaries_path = os.path.join(in_device_path, "Binaries") if not os.path.isdir(in_device_binaries_path): diff --git a/Buildscripts/gh-download-artifacts.py b/Buildscripts/gh-download-artifacts.py new file mode 100755 index 00000000..62cb1583 --- /dev/null +++ b/Buildscripts/gh-download-artifacts.py @@ -0,0 +1,135 @@ +#!/usr/bin/env python3 +"""Download all artifacts from a GitHub Actions workflow run.""" + +import argparse +import os +import subprocess +import sys +import zipfile +from pathlib import Path + +import requests + +API_URL = "https://api.github.com" + + +def get_token(cli_token: str | None) -> str: + if cli_token: + return cli_token + env_token = os.environ.get("GITHUB_TOKEN") or os.environ.get("GH_TOKEN") + if env_token: + return env_token + try: + result = subprocess.run( + ["gh", "auth", "token"], capture_output=True, text=True, check=True + ) + return result.stdout.strip() + except (FileNotFoundError, subprocess.CalledProcessError): + sys.exit( + "No token found. Pass --token, set GITHUB_TOKEN/GH_TOKEN, or run `gh auth login`." + ) + + +def get_repo(cli_repo: str | None) -> str: + if cli_repo: + return cli_repo + try: + result = subprocess.run( + ["git", "remote", "get-url", "origin"], + capture_output=True, + text=True, + check=True, + ) + url = result.stdout.strip() + except (FileNotFoundError, subprocess.CalledProcessError): + sys.exit("Could not detect repo from git remote. Pass --repo owner/name.") + + # Handle both SSH and HTTPS remote URLs. + if url.startswith("git@"): + path = url.split(":", 1)[1] + else: + path = url.split("github.com/", 1)[-1] + return path.removesuffix(".git") + + +def download_artifacts(repo: str, run_id: str, token: str, out_dir: Path): + headers = { + "Authorization": f"Bearer {token}", + "Accept": "application/vnd.github+json", + "X-GitHub-Api-Version": "2022-11-28", + } + + out_dir.mkdir(parents=True, exist_ok=True) + + url = f"{API_URL}/repos/{repo}/actions/runs/{run_id}/artifacts" + artifacts = [] + while url: + resp = requests.get(url, headers=headers, params={"per_page": 100}) + resp.raise_for_status() + data = resp.json() + artifacts.extend(data["artifacts"]) + url = resp.links.get("next", {}).get("url") + + if not artifacts: + print(f"No artifacts found for run {run_id}.") + return + + for artifact in artifacts: + name = artifact["name"] + if name == "all-artifacts": + print(f"Skipping {name}") + continue + if artifact.get("expired"): + print(f"Skipping expired artifact: {name}") + continue + + print(f"Downloading {name} ({artifact['size_in_bytes']} bytes)...") + download_url = artifact["archive_download_url"] + resp = requests.get(download_url, headers=headers, stream=True) + resp.raise_for_status() + + zip_path = out_dir / f"{name}.zip" + with open(zip_path, "wb") as f: + for chunk in resp.iter_content(chunk_size=8192): + f.write(chunk) + + if name.endswith(".bin"): + with zipfile.ZipFile(zip_path) as zf: + zf.extractall(out_dir) + zip_path.unlink() + print(f" Extracted to {out_dir}") + else: + print(f" Saved to {zip_path}") + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("run_id", help="GitHub Actions run ID") + parser.add_argument( + "--repo", + help="owner/repo (default: detected from git remote 'origin')", + ) + parser.add_argument( + "--token", + help="GitHub token (default: $GITHUB_TOKEN, $GH_TOKEN, or `gh auth token`)", + ) + parser.add_argument( + "--out", + default="artifacts", + help="Output directory (default: ./artifacts)", + ) + args = parser.parse_args() + + repo = get_repo(args.repo) + token = get_token(args.token) + out_dir = Path(args.out) + + print(f"Repo: {repo}") + print(f"Run ID: {args.run_id}") + print(f"Output: {out_dir.resolve()}") + + download_artifacts(repo, args.run_id, token, out_dir) + + +if __name__ == "__main__": + main() diff --git a/Buildscripts/gh-release-summary.py b/Buildscripts/gh-release-summary.py new file mode 100755 index 00000000..004f49fa --- /dev/null +++ b/Buildscripts/gh-release-summary.py @@ -0,0 +1,126 @@ +#!/usr/bin/env python3 +"""Summarize commits between the last 2 pushed git tags into commits.md and pull-requests.md.""" + +import argparse +import re +import subprocess +import sys +from pathlib import Path + +PR_REF_RE = re.compile(r"#(\d+)") + + +def get_repo(cli_repo: str | None) -> str: + if cli_repo: + return cli_repo + try: + result = subprocess.run( + ["git", "remote", "get-url", "origin"], + capture_output=True, + text=True, + check=True, + ) + url = result.stdout.strip() + except (FileNotFoundError, subprocess.CalledProcessError): + sys.exit("Could not detect repo from git remote. Pass --repo owner/name.") + + if url.startswith("git@"): + path = url.split(":", 1)[1] + else: + path = url.split("github.com/", 1)[-1] + return path.removesuffix(".git") + + +def get_last_two_tags() -> tuple[str, str]: + result = subprocess.run( + ["git", "tag", "--sort=-creatordate"], + capture_output=True, + text=True, + check=True, + ) + tags = [t for t in result.stdout.splitlines() if t] + if len(tags) < 2: + sys.exit(f"Need at least 2 tags, found {len(tags)}.") + return tags[1], tags[0] # previous, latest + + +def get_commits(prev_tag: str, latest_tag: str) -> list[tuple[str, str]]: + result = subprocess.run( + [ + "git", + "log", + "--pretty=format:%H\x1f%s", + f"{prev_tag}..{latest_tag}", + ], + capture_output=True, + text=True, + check=True, + ) + commits = [] + for line in result.stdout.splitlines(): + if not line: + continue + sha, subject = line.split("\x1f", 1) + commits.append((sha, subject)) + return commits + + +def write_commits_md(path: Path, repo: str, prev_tag: str, latest_tag: str, commits: list[tuple[str, str]]): + lines = [f"# Commits: {prev_tag}..{latest_tag}", ""] + for sha, subject in commits: + link = f"https://github.com/{repo}/commit/{sha}" + lines.append(f"- [{subject}]({link})") + path.write_text("\n".join(lines) + "\n") + + +def write_pull_requests_md(path: Path, repo: str, commits: list[tuple[str, str]]): + pr_numbers = [] + seen = set() + for _, subject in commits: + for match in PR_REF_RE.finditer(subject): + number = match.group(1) + if number not in seen: + seen.add(number) + pr_numbers.append(number) + + lines = ["# Pull Requests", ""] + for number in pr_numbers: + lines.append(f"- https://github.com/{repo}/pull/{number}") + path.write_text("\n".join(lines) + "\n") + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument( + "--repo", + help="owner/repo (default: detected from git remote 'origin')", + ) + parser.add_argument( + "--out", + default=".", + help="Output directory for commits.md and pull-requests.md (default: current directory)", + ) + args = parser.parse_args() + + repo = get_repo(args.repo) + prev_tag, latest_tag = get_last_two_tags() + commits = get_commits(prev_tag, latest_tag) + + out_dir = Path(args.out) + out_dir.mkdir(parents=True, exist_ok=True) + + commits_path = out_dir / "release-commits.md" + prs_path = out_dir / "release-pull-requests.md" + + write_commits_md(commits_path, repo, prev_tag, latest_tag, commits) + write_pull_requests_md(prs_path, repo, commits) + + print(f"Repo: {repo}") + print(f"Tags: {prev_tag}..{latest_tag}") + print(f"Commits: {len(commits)}") + print(f"Wrote {commits_path}") + print(f"Wrote {prs_path}") + + +if __name__ == "__main__": + main() diff --git a/Buildscripts/properties.cmake b/Buildscripts/properties.cmake index e25beaa5..ed7eea05 100644 --- a/Buildscripts/properties.cmake +++ b/Buildscripts/properties.cmake @@ -28,7 +28,6 @@ function(READ_PROPERTIES_TO_MAP PROPERTY_FILE RESULT_VAR) endif () file(STRINGS ${PROPERTY_FILE_ABS} lines) - set(current_section "") set(map_content "") foreach(line IN LISTS lines) @@ -37,9 +36,7 @@ function(READ_PROPERTIES_TO_MAP PROPERTY_FILE RESULT_VAR) continue() endif () - if (line MATCHES "^\\[.*\\]$") - set(current_section "${line}") - elseif (line MATCHES "^([^=]+)=(.*)$") + if (line MATCHES "^([^=]+)=(.*)$") set(key "${CMAKE_MATCH_1}") set(value "${CMAKE_MATCH_2}") string(STRIP "${key}" key) @@ -49,7 +46,7 @@ function(READ_PROPERTIES_TO_MAP PROPERTY_FILE RESULT_VAR) set(value "${CMAKE_MATCH_1}") endif () - list(APPEND map_content "${current_section}${key}" "${value}") + list(APPEND map_content "${key}" "${value}") endif () endforeach() diff --git a/Buildscripts/release-summary.py b/Buildscripts/release-summary.py new file mode 100755 index 00000000..f3918dee --- /dev/null +++ b/Buildscripts/release-summary.py @@ -0,0 +1,84 @@ +#!/usr/bin/env python3 +"""Generate a release summary markdown by combining gh-release-summary.py output with Claude.""" + +import argparse +import subprocess +import sys +from pathlib import Path + +SCRIPT_DIR = Path(__file__).resolve().parent + +SECTIONS = [ + "New Devices", + "New Features", + "New Apps", + "Improvements & Fixes", + "Other Changes", + "TactilitySDK", + "Known Issues", +] + + +def run_gh_release_summary(out_dir: Path, repo: str | None): + cmd = [sys.executable, str(SCRIPT_DIR / "gh-release-summary.py"), "--out", str(out_dir)] + if repo: + cmd += ["--repo", repo] + subprocess.run(cmd, check=True) + + +def build_prompt() -> str: + section_list = "\n".join(f"- {s}" for s in SECTIONS) + return ( + "You are given a list of commits (with GitHub commit links) and a list of pull " + "request links from a software release. Write a release summary in Markdown, " + "organizing changes into these sections (omit a section if it has no relevant " + "changes):\n\n" + f"{section_list}\n\n" + "Use concise bullet points. Base every bullet strictly on the given commits and " + "pull requests, referencing the pull request link where applicable. Output only " + "the Markdown, no commentary before or after it." + "If a pull request or commit has multiple changes add them as separate entries." + "Ignore irrelevant changes: documentation, minor fixes, non-descript fixes, scripts, github workflows." + "For 'New Devices': only mention the device name, don't like pull request." + "For 'Improvements & Fixes': Avoid non-descript entries, write specifics." + "For 'TactilitySDK': Avoid generic changes, be specific. Mention API changes." + ) + + +def run_claude(commits_md: str, prs_md: str) -> str: + prompt = build_prompt() + stdin_content = f"## Commits\n\n{commits_md}\n\n## Pull Requests\n\n{prs_md}" + result = subprocess.run( + ["claude", "-p", prompt], + input=stdin_content, + capture_output=True, + text=True, + check=True, + ) + return result.stdout + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--repo", help="owner/repo (default: detected from git remote 'origin')") + parser.add_argument("--out", default=".", help="Output directory (default: current directory)") + args = parser.parse_args() + + out_dir = Path(args.out) + out_dir.mkdir(parents=True, exist_ok=True) + + run_gh_release_summary(out_dir, args.repo) + + commits_md = (out_dir / "release-commits.md").read_text() + prs_md = (out_dir / "release-pull-requests.md").read_text() + + print("Generating release summary via claude...") + summary = run_claude(commits_md, prs_md) + + summary_path = out_dir / "release-summary.md" + summary_path.write_text(summary) + print(f"Wrote {summary_path}") + + +if __name__ == "__main__": + main() diff --git a/Buildscripts/sdkconfig/default.properties b/Buildscripts/sdkconfig/default.properties index 0aeaabcf..4bb2689b 100644 --- a/Buildscripts/sdkconfig/default.properties +++ b/Buildscripts/sdkconfig/default.properties @@ -9,7 +9,8 @@ CONFIG_FREERTOS_PLACE_FUNCTIONS_INTO_FLASH=y CONFIG_FREERTOS_PLACE_SNAPSHOT_FUNS_INTO_FLASH=y CONFIG_RINGBUF_PLACE_FUNCTIONS_INTO_FLASH=y # EmbedTLS -CONFIG_MBEDTLS_SSL_PROTO_TLS1_3=y +# Use TLS 1.2 because 1.3 conflicts with MbedTLS dynamic buffer +CONFIG_MBEDTLS_SSL_PROTO_TLS1_2=y # LVGL CONFIG_LV_USE_USER_DATA=y CONFIG_LV_USE_FS_STDIO=y @@ -40,3 +41,17 @@ CONFIG_WL_SECTOR_MODE_SAFE=y CONFIG_WL_SECTOR_MODE=1 # Allow new i2c_master API (used by Tab5Keyboard for LP_I2C_NUM_0) to coexist with legacy i2c driver CONFIG_I2C_SKIP_LEGACY_CONFLICT_CHECK=y +# Wi-Fi memory optimizations +CONFIG_ESP_WIFI_STATIC_RX_BUFFER_NUM=3 +CONFIG_ESP_WIFI_DYNAMIC_RX_BUFFER_NUM=5 +CONFIG_ESP_WIFI_STATIC_TX_BUFFER=y +CONFIG_ESP_WIFI_TX_BUFFER_TYPE=0 +CONFIG_ESP_WIFI_STATIC_TX_BUFFER_NUM=5 +CONFIG_ESP_WIFI_CACHE_TX_BUFFER_NUM=5 +CONFIG_ESP_WIFI_STATIC_RX_MGMT_BUFFER=y +CONFIG_ESP_WIFI_DYNAMIC_RX_MGMT_BUF=0 +CONFIG_ESP_WIFI_RX_MGMT_BUF_NUM_DEF=5 +CONFIG_ESP_WIFI_AMPDU_TX_ENABLED=y +CONFIG_ESP_WIFI_TX_BA_WIN=5 +CONFIG_ESP_WIFI_AMPDU_RX_ENABLED=y +CONFIG_ESP_WIFI_RX_BA_WIN=5 diff --git a/CMakeLists.txt b/CMakeLists.txt index 0e46d3be..0ea8524e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -74,8 +74,11 @@ else () message("Building for sim target") add_compile_definitions(CONFIG_TT_DEVICE_ID="simulator") add_compile_definitions(CONFIG_TT_DEVICE_NAME="Simulator") + add_compile_definitions(CONFIG_TT_DEVICE_VENDOR="") + add_compile_definitions(CONFIG_TT_DEVICE_NAME_SIMPLE="Simulator") add_compile_definitions(CONFIG_TT_LAUNCHER_APP_ID="Launcher") add_compile_definitions(CONFIG_TT_AUTO_START_APP_ID="") + add_compile_definitions(CONFIG_TT_USER_DATA_LOCATION_INTERNAL) endif () project(Tactility) diff --git a/Data/data/service/bluetooth/settings.properties b/Data/data/service/bluetooth/settings.properties deleted file mode 100644 index 583f5735..00000000 --- a/Data/data/service/bluetooth/settings.properties +++ /dev/null @@ -1 +0,0 @@ -enableOnBoot=false \ No newline at end of file diff --git a/Data/data/service/wifi/settings.properties b/Data/data/service/wifi/settings.properties deleted file mode 100644 index 583f5735..00000000 --- a/Data/data/service/wifi/settings.properties +++ /dev/null @@ -1 +0,0 @@ -enableOnBoot=false \ No newline at end of file diff --git a/Data/data/settings/development.properties b/Data/data/settings/development.properties deleted file mode 100644 index 583f5735..00000000 --- a/Data/data/settings/development.properties +++ /dev/null @@ -1 +0,0 @@ -enableOnBoot=false \ No newline at end of file diff --git a/Data/data/settings/system.properties b/Data/data/settings/system.properties deleted file mode 100644 index 8f251bec..00000000 --- a/Data/data/settings/system.properties +++ /dev/null @@ -1,5 +0,0 @@ -language=en-US -timeFormat24h=true -dateFormat=DD/MM/YYYY -region=EU -timezone=Europe/Amsterdam \ No newline at end of file diff --git a/Data/data/service/webserver/settings.properties b/Data/data/tactility/service/webserver/settings.properties similarity index 100% rename from Data/data/service/webserver/settings.properties rename to Data/data/tactility/service/webserver/settings.properties diff --git a/Data/data/webserver/dashboard.html b/Data/system/app/WebServer/dashboard.html similarity index 99% rename from Data/data/webserver/dashboard.html rename to Data/system/app/WebServer/dashboard.html index 69553e72..d7e75ec5 100644 --- a/Data/data/webserver/dashboard.html +++ b/Data/system/app/WebServer/dashboard.html @@ -485,7 +485,7 @@
- +