From 4cd9a70d89dd8bcea54298695d9bf65740f9589e Mon Sep 17 00:00:00 2001 From: Jakub Andrysek Date: Fri, 24 Oct 2025 16:25:31 +0200 Subject: [PATCH 01/11] feat(docs): Add documentation build examples script and update CI workflows --- .github/scripts/docs_build_examples.py | 499 ++++++++++++++++++ .github/scripts/sketch_utils.sh | 84 ++- .github/workflows/docs_build.yml | 34 +- .github/workflows/docs_deploy.yml | 33 +- docs/conf_common.py | 1 + docs/en/api/gpio.rst | 20 +- docs/en/conf.py | 7 + docs/requirements.txt | 8 +- libraries/ESP32/examples/GPIO/Blink/Blink.ino | 16 + libraries/ESP32/examples/GPIO/Blink/ci.yml | 78 +++ 10 files changed, 738 insertions(+), 42 deletions(-) create mode 100755 .github/scripts/docs_build_examples.py create mode 100644 libraries/ESP32/examples/GPIO/Blink/Blink.ino create mode 100644 libraries/ESP32/examples/GPIO/Blink/ci.yml diff --git a/.github/scripts/docs_build_examples.py b/.github/scripts/docs_build_examples.py new file mode 100755 index 00000000000..9c3f1a63674 --- /dev/null +++ b/.github/scripts/docs_build_examples.py @@ -0,0 +1,499 @@ +#!/usr/bin/env python3 +""" +Documentation build examples script for ESP32 Arduino. + +This script manages binary preparation and cleanup for documentation examples. +It can build examples with upload-binary configuration, clean up the binaries +directory, and optionally generate diagrams and LaunchPad configurations. + +The script requires Arduino CLI and user paths when building examples. +It processes all sketches that have 'upload-binary' configuration in their +ci.json files and builds them for specified targets. + +Usage: + docs_build_examples.py -c # Clean up + docs_build_examples.py --build -ai /path/cli -au /path/user # Build all + docs_build_examples.py --build -ai /path/cli -au /path/user --diagram --launchpad # Build with extras + +Environment Variables: + ARDUINO_ESP32_PATH: Path to ESP32 Arduino core + GITHUB_WORKSPACE: GitHub workspace path (fallback) + DOCS_PROD_URL_BASE: Base URL for documentation deployment + REPO_URL_PREFIX: Repository URL prefix for LaunchPad configs +""" + +import argparse +from argparse import RawDescriptionHelpFormatter +from esp_docs.generic_extensions.docs_embed.tool.wokwi_tool import DiagramSync +import os +import shutil +import sys +from pathlib import Path +import subprocess +import yaml +import platform + +SCRIPT_DIR = Path(__file__).resolve().parent + +ARDUINO_ESP32_PATH = os.environ.get("ARDUINO_ESP32_PATH") +GITHUB_WORKSPACE = os.environ.get("GITHUB_WORKSPACE") +STORAGE_URL_PREFIX = os.environ.get("STORAGE_URL_PREFIX") +REPO_URL_PREFIX = os.environ.get("REPO_URL_PREFIX") + +if ARDUINO_ESP32_PATH and (Path(ARDUINO_ESP32_PATH) / "tools" / "esp32-arduino-libs").is_dir(): + SDKCONFIG_DIR = Path(ARDUINO_ESP32_PATH) / "tools" / "esp32-arduino-libs" +elif GITHUB_WORKSPACE and (Path(GITHUB_WORKSPACE) / "tools" / "esp32-arduino-libs").is_dir(): + SDKCONFIG_DIR = Path(GITHUB_WORKSPACE) / "tools" / "esp32-arduino-libs" +else: + SDKCONFIG_DIR = Path("tools/esp32-arduino-libs") + +KEEP_FILES = [ + "*.merged.bin", + "ci.yml", + "launchpad.toml", + "diagram*.json", + ".gitignore", +] + +SKETCH_UTILS = SCRIPT_DIR / "sketch_utils.sh" +DOCS_BINARIES_DIR = Path("docs/_static/binaries") + + +def detect_arduino_paths(): + """Get Arduino CLI and user paths from environment variables set by install-arduino-cli.sh + + The function will create the Arduino user directory if it doesn't exist. + + Returns: + tuple: (arduino_cli_path, arduino_user_path) or (None, None) if not found + """ + try: + # Get paths from environment variables exported by install-arduino-cli.sh + arduino_ide_path = os.environ.get("ARDUINO_IDE_PATH") + arduino_usr_path = os.environ.get("ARDUINO_USR_PATH") + + if not arduino_ide_path or not arduino_usr_path: + print("Arduino paths not found in environment variables.") + print("Make sure to run install-arduino-cli.sh first to set ARDUINO_IDE_PATH and ARDUINO_USR_PATH") + return None, None + + # Convert to Path objects for validation + ide_path = Path(arduino_ide_path) + usr_path = Path(arduino_usr_path) + + # Check if arduino-cli exists + arduino_cli_exe = ide_path / "arduino-cli" + if platform.system().lower() == "windows": + arduino_cli_exe = ide_path / "arduino-cli.exe" + + if arduino_cli_exe.exists(): + # Create user path if it doesn't exist + if not usr_path.exists(): + try: + usr_path.mkdir(parents=True, exist_ok=True) + print(f"Created Arduino user directory: {usr_path}") + except Exception as e: + print(f"Failed to create Arduino user directory {usr_path}: {e}") + return None, None + + return str(ide_path), str(usr_path) + else: + print(f"Arduino CLI or user path not found:") + print(f" Arduino CLI: {arduino_cli_exe} {'✓' if arduino_cli_exe.exists() else '✗'}") + print(f" User path: {usr_path} {'✓' if usr_path.exists() else '✗'}") + return None, None + + except Exception as e: + print(f"Error getting Arduino paths from environment: {e}") + return None, None +def run_cmd(cmd, check=True, capture_output=False, text=True): + """Execute a shell command with error handling. + + Args: + cmd (list): Command and arguments to execute + check (bool): Whether to raise exception on non-zero exit code + capture_output (bool): Whether to capture stdout/stderr + text (bool): Whether to return text output instead of bytes + """ + try: + return subprocess.run(cmd, check=check, capture_output=capture_output, text=text) + except subprocess.CalledProcessError as e: + # CalledProcessError is raised only when check=True and the command exits non-zero + print(f"ERROR: Command failed: {' '.join(cmd)}") + print(f"Exit code: {e.returncode}") + if hasattr(e, 'stdout') and e.stdout: + print("--- stdout ---") + print(e.stdout) + if hasattr(e, 'stderr') and e.stderr: + print("--- stderr ---") + print(e.stderr) + # Exit the whole script with the same return code to mimic shell behavior + sys.exit(e.returncode) + except FileNotFoundError: + print(f"ERROR: Command not found: {cmd[0]}") + sys.exit(127) + + +def check_requirements(sketch_dir, sdkconfig_path): + """Check if sketch meets requirements for the given SDK config. + + Args: + sketch_dir (str): Path to the sketch directory + sdkconfig_path (str): Path to the SDK config file + + Returns: + bool: True if requirements are met, False otherwise + """ + cmd = [str(SKETCH_UTILS), "check_requirements", sketch_dir, str(sdkconfig_path)] + try: + res = run_cmd(cmd, check=False, capture_output=True) + return res.returncode == 0 + except Exception: + return False + + +def install_libs(*args): + """Install Arduino libraries using sketch_utils.sh""" + cmd = [str(SKETCH_UTILS), "install_libs"] + list(args) + return run_cmd(cmd, check=False) + + +def build_sketch(args_list): + """Build a sketch using sketch_utils.sh""" + cmd = [str(SKETCH_UTILS), "build"] + args_list + return run_cmd(cmd, check=False) + + +def parse_args(argv): + """Parse command line arguments""" + epilog_text = ( + "Examples:\n" + " docs_build_examples.py -c # Clean up binaries directory\n" + " docs_build_examples.py --build # Build all examples (use env vars)\n" + " docs_build_examples.py --build -ai /path/to/cli -au /path/to/user # Build with explicit paths\n" + " docs_build_examples.py --build --diagram --launchpad # Build with diagrams and LaunchPad\n\n" + "Path detection:\n" + " Arduino paths are read from ARDUINO_IDE_PATH and ARDUINO_USR_PATH environment variables\n" + " Set by running install-arduino-cli.sh first, or use -ai/-au to override\n\n" + ) + + p = argparse.ArgumentParser( + description="Build examples that have ci.yml with upload-binary targets", + formatter_class=RawDescriptionHelpFormatter, + epilog=epilog_text, + ) + p.add_argument( + "-c", "--cleanup", + dest="cleanup", + action="store_true", + help="Clean up docs binaries directory and exit", + ) + p.add_argument( + "-ai", "--arduino-cli-path", + dest="arduino_cli_path", + help="Path to Arduino CLI installation directory (overrides ARDUINO_IDE_PATH env var)", + ) + p.add_argument( + "-au", "--arduino-user-path", + dest="user_path", + help="Path to Arduino user directory (overrides ARDUINO_USR_PATH env var)", + ) + p.add_argument( + "-b", "--build", + dest="build", + action="store_true", + help="Build all examples", + ) + p.add_argument( + "-d", "--diagram", + dest="generate_diagrams", + action="store_true", + help="Generate diagrams for prepared examples using docs-embed", + ) + p.add_argument( + "-l", "--launchpad", + dest="generate_launchpad_config", + action="store_true", + help="Generate LaunchPad config for prepared examples", + ) + return p.parse_args(argv) + + +def validate_prerequisites(args): + """Validate that required prerequisites are available and get paths from env vars if needed.""" + + # Get paths from environment variables if not provided via arguments + if not args.arduino_cli_path or not args.user_path: + print("Getting Arduino paths from environment variables...") + detected_cli_path, detected_user_path = detect_arduino_paths() + + if not args.arduino_cli_path: + if detected_cli_path: + args.arduino_cli_path = detected_cli_path + print(f" Arduino CLI path (ARDUINO_IDE_PATH): {detected_cli_path}") + else: + print("ERROR: Arduino CLI path not found in ARDUINO_IDE_PATH env var and not provided (-ai option)") + print("Run install-arduino-cli.sh first to set environment variables") + sys.exit(1) + + if not args.user_path: + if detected_user_path: + args.user_path = detected_user_path + print(f" Arduino user path (ARDUINO_USR_PATH): {detected_user_path}") + else: + print("ERROR: Arduino user path not found in ARDUINO_USR_PATH env var and not provided (-au option)") + print("Run install-arduino-cli.sh first to set environment variables") + sys.exit(1) + + # Validate the paths + arduino_cli_exe = Path(args.arduino_cli_path) / "arduino-cli" + if platform.system().lower() == "windows": + arduino_cli_exe = Path(args.arduino_cli_path) / "arduino-cli.exe" + + if not arduino_cli_exe.exists(): + print(f"ERROR: arduino-cli not found at {arduino_cli_exe}") + sys.exit(1) + + # Create Arduino user path if it doesn't exist + user_path = Path(args.user_path) + if not user_path.is_dir(): + try: + user_path.mkdir(parents=True, exist_ok=True) + print(f"Created Arduino user directory: {user_path}") + except Exception as e: + print(f"ERROR: Failed to create Arduino user path {user_path}: {e}") + sys.exit(1) +def cleanup_binaries(): + """Clean up the binaries directory, keeping only specified files. + + Removes all files except those matching patterns in KEEP_FILES. + Also removes empty directories after cleanup. + """ + print(f"Cleaning up binaries directory: {DOCS_BINARIES_DIR}") + if not DOCS_BINARIES_DIR.exists(): + print("Binaries directory does not exist, nothing to clean") + return + for root, dirs, files in os.walk(DOCS_BINARIES_DIR): + for fname in files: + fpath = Path(root) / fname + parent = Path(root).name + # Always remove sketch/ci.yml + if parent == "sketch" and fname == "ci.yml": + fpath.unlink() + continue + keep = False + for pattern in KEEP_FILES: + if Path(fname).match(pattern): + keep = True + break + if not keep: + print(f"Removing: {fpath}") + fpath.unlink() + else: + print(f"Keeping: {fpath}") + # remove empty dirs + for root, dirs, files in os.walk(DOCS_BINARIES_DIR, topdown=False): + if not os.listdir(root): + try: + os.rmdir(root) + except Exception: + pass + print("Cleanup completed") + + +def find_examples_with_upload_binary(): + """Find all Arduino sketches that have upload-binary configuration. + + Returns: + list: List of paths to .ino files that have upload-binary in ci.yml + """ + res = [] + for ino in Path('.').rglob('*.ino'): + sketch_dir = ino.parent + sketch_name = ino.stem + dir_name = sketch_dir.name + if dir_name != sketch_name: + continue + ci_yml = sketch_dir / 'ci.yml' + if ci_yml.exists(): + try: + data = yaml.safe_load(ci_yml.read_text()) + if 'upload-binary' in data and data['upload-binary']: + res.append(str(ino)) + except Exception: + continue + return res + + +def get_upload_binary_targets(sketch_dir): + """Get the upload-binary targets from a sketch's ci.yml file. + + Args: + sketch_dir (str or Path): Path to the sketch directory + + Returns: + list: List of target names for upload-binary, empty if none found + """ + ci_yml = Path(sketch_dir) / 'ci.yml' + try: + data = yaml.safe_load(ci_yml.read_text()) + targets = data.get('upload-binary', {}).get('targets', []) + return targets + except Exception: + return [] + + +def build_example_for_target(sketch_dir, target, relative_path, args): + """Build a single example for a specific target. + + Args: + sketch_dir (Path): Path to the sketch directory + target (str): Target board/configuration name + relative_path (str): Relative path for output organization + args (argparse.Namespace): Parsed command line arguments + + Returns: + bool: True if build succeeded, False otherwise + """ + print(f"\n > Building example: {relative_path} for target: {target}") + output_dir = DOCS_BINARIES_DIR / relative_path / target + output_dir.mkdir(parents=True, exist_ok=True) + + sdkconfig = SDKCONFIG_DIR / target / 'sdkconfig' + if not check_requirements(str(sketch_dir), sdkconfig): + print(f"Target {target} does not meet the requirements for {Path(sketch_dir).name}. Skipping.") + return True + + # Build the sketch using sketch_utils.sh build - pass args as in shell script + build_args = [ + "-ai", + args.arduino_cli_path, + "-au", + args.user_path, + "-s", + str(sketch_dir), + "-t", + target, + "-b", + str(output_dir), + "--first-only", + ] + res = build_sketch(build_args) + if res.returncode == 0: + print(f"Successfully built {relative_path} for {target}") + ci_yml = Path(sketch_dir) / 'ci.yml' + if ci_yml.exists(): + shutil.copy(ci_yml, output_dir / 'ci.yml') + if args.generate_diagrams: + print(f"Generating diagram for {relative_path} ({target})...") + try: + sync = DiagramSync(output_dir) + sync.generate_diagram_from_ci(target) + except Exception as e: + print(f"WARNING: Failed to generate diagram for {relative_path} ({target}): {e}") + else: + print(f"ERROR: Failed to build {relative_path} for {target}") + return False + return True + + +def build_all_examples(args): + """Build all examples that have upload-binary configuration + + Prerequisites are validated in main() before calling this function + """ + total_built = 0 + total_failed = 0 + + if DOCS_BINARIES_DIR.exists(): + shutil.rmtree(DOCS_BINARIES_DIR) + print(f"Removed existing build directory: {DOCS_BINARIES_DIR}") + + # add gitignore to binaries dir with * for new files + DOCS_BINARIES_DIR.mkdir(parents=True, exist_ok=True) + gitignore_path = DOCS_BINARIES_DIR / '.gitignore' + gitignore_path.write_text("*\n") + + examples = find_examples_with_upload_binary() + if not examples: + print("No examples found with upload-binary configuration") + return 0 + + print('\nExamples to be built:') + print('====================') + for i, example in enumerate(examples, start=1): + sketch_dir = Path(example).parent + relative_path = str(sketch_dir).lstrip('./') + targets = get_upload_binary_targets(sketch_dir) + if targets: + print(f"{i}. {relative_path} (targets: {' '.join(targets)})") + print() + + for example in examples: + sketch_dir = Path(example).parent + relative_path = str(sketch_dir).lstrip('./') + targets = get_upload_binary_targets(sketch_dir) + if not targets: + print(f"WARNING: No targets found for {relative_path}") + continue + print(f"Building {relative_path} for targets: {targets}") + for target in targets: + ok = build_example_for_target(sketch_dir, target, relative_path, args) + if ok is not False: # Handle None return as success + total_built += 1 + else: + total_failed += 1 + + output_sketch_dir = DOCS_BINARIES_DIR / relative_path + output_sketch_dir.mkdir(parents=True, exist_ok=True) + + # copy sketch ci.yml to output dir - parent of target dirs + ci_yml = sketch_dir / 'ci.yml' + if ci_yml.exists(): + shutil.copy(ci_yml, output_sketch_dir / 'ci.yml') + + if args.generate_launchpad_config: + print(f"Generating LaunchPad config for {relative_path}/{target}...") + try: + sync = DiagramSync(output_sketch_dir / target) + sync.generate_launchpad_config(STORAGE_URL_PREFIX, REPO_URL_PREFIX, True, output_sketch_dir) + except Exception as e: + print(f"WARNING: Failed to generate LaunchPad config for {relative_path}/{target}: {e}") + + print('\nBuild summary:') + print(f" Successfully built: {total_built}") + print(f" Failed builds: {total_failed}") + print(f" Output directory: {DOCS_BINARIES_DIR}") + return total_failed + + +def main(argv): + """Main entry point for the script""" + args = parse_args(argv) + + if args.cleanup: + cleanup_binaries() + return + + if args.build: + # Validate prerequisites and auto-detect paths if needed + validate_prerequisites(args) + + result = build_all_examples(args) + if result == 0: + print('\nAll examples built successfully!') + else: + print('\nSome builds failed. Check the output above for details.') + sys.exit(1) + return + + if args.generate_diagrams or args.generate_launchpad_config: + print("ERROR: --diagram and --launchpad options are only available when building examples (--build)") + sys.exit(1) + + # If no specific action is requested, show help + parse_args(['--help']) + + +if __name__ == '__main__': + main(sys.argv[1:]) diff --git a/.github/scripts/sketch_utils.sh b/.github/scripts/sketch_utils.sh index 7452e494084..5cc47a69df8 100755 --- a/.github/scripts/sketch_utils.sh +++ b/.github/scripts/sketch_utils.sh @@ -54,6 +54,8 @@ function check_requirements { # check_requirements } function build_sketch { # build_sketch [extra-options] + local first_only=false + while [ -n "$1" ]; do case "$1" in -ai ) @@ -92,6 +94,36 @@ function build_sketch { # build_sketch [ext shift debug_level="DebugLevel=$1" ;; + -b ) + shift + custom_build_dir=$1 + ;; + --first-only ) + first_only=true + ;; + -h ) + echo "Usage: build_sketch [options]" + echo "" + echo "Build an Arduino sketch for ESP32 targets." + echo "" + echo "Required options:" + echo " -ai Arduino IDE path" + echo " -au Arduino user path" + echo " -s Sketch directory containing .ino file and ci.yml" + echo " -t Target chip (esp32, esp32s2, esp32c3, esp32s3, esp32c6, esp32h2, esp32p4, esp32c5)" + echo " Required unless -fqbn is specified" + echo "" + echo "Optional options:" + echo " -fqbn Fully qualified board name (alternative to -t)" + echo " -o Board options (PSRAM, USBMode, etc.) [default: chip-specific]" + echo " -i Chunk index for parallel builds [default: none]" + echo " -l Log compilation output to JSON file [default: none]" + echo " -d Debug level (DebugLevel=...) [default: none]" + echo " -b Custom build directory [default: \$ARDUINO_BUILD_DIR or \$HOME/.arduino/tests/\$target/\$sketchname/build.tmp]" + echo " --first-only Build only the first FQBN from ci.yml configurations [default: false]" + echo " -h Show this help message" + exit 0 + ;; * ) break ;; @@ -128,7 +160,15 @@ function build_sketch { # build_sketch [ext len=$(yq eval ".fqbn.${target} | length" "$sketchdir"/ci.yml 2>/dev/null || echo 0) if [ "$len" -gt 0 ]; then - fqbn=$(yq eval ".fqbn.${target} | sort | @json" "$sketchdir"/ci.yml) + if [ "$first_only" = true ]; then + # Get only the first FQBN from the array (original order) + fqbn=$(yq eval ".fqbn.${target} | .[0]" "$sketchdir"/ci.yml 2>/dev/null) + fqbn="[\"$fqbn\"]" + len=1 + else + # Build all FQBNs + fqbn=$(yq eval ".fqbn.${target} | sort | @json" "$sketchdir"/ci.yml) + fi fi fi @@ -219,12 +259,14 @@ function build_sketch { # build_sketch [ext fi # The directory that will hold all the artifacts (the build directory) is - # provided through: - # 1. An env variable called ARDUINO_BUILD_DIR. - # 2. Created at the sketch level as "build" in the case of a single - # configuration test. - # 3. Created at the sketch level as "buildX" where X is the number - # of configuration built in case of a multiconfiguration test. + # determined by the following priority: + # 1. Custom build directory via -b flag: + # - If path contains "docs/_static/binaries", use as exact path (for docs builds) + # - Otherwise, append target name to custom directory + # 2. ARDUINO_BUILD_DIR environment variable (if set) + # 3. Default: $HOME/.arduino/tests/$target/$sketchname/build.tmp + # + # For multiple configurations, subsequent builds use .1, .2, etc. suffixes sketchname=$(basename "$sketchdir") local has_requirements @@ -253,11 +295,19 @@ function build_sketch { # build_sketch [ext fi ARDUINO_CACHE_DIR="$HOME/.arduino/cache.tmp" - if [ -n "$ARDUINO_BUILD_DIR" ]; then - build_dir="$ARDUINO_BUILD_DIR" - elif [ "$len" -eq 1 ]; then - # build_dir="$sketchdir/build" - build_dir="$HOME/.arduino/tests/$target/$sketchname/build.tmp" + + # Determine base build directory + if [ -n "$custom_build_dir" ]; then + # If custom_build_dir contains docs/_static/binaries, use it as exact path + if [[ "$custom_build_dir" == *"docs/_static/binaries"* ]]; then + build_dir_base="$custom_build_dir" + else + build_dir_base="$custom_build_dir/$target" + fi + elif [ -n "$ARDUINO_BUILD_DIR" ]; then + build_dir_base="$ARDUINO_BUILD_DIR" + else + build_dir_base="$HOME/.arduino/tests/$target/$sketchname/build.tmp" fi output_file="$HOME/.arduino/cli_compile_output.txt" @@ -265,10 +315,14 @@ function build_sketch { # build_sketch [ext mkdir -p "$ARDUINO_CACHE_DIR" for i in $(seq 0 $((len - 1))); do - if [ "$len" -ne 1 ]; then - # build_dir="$sketchdir/build$i" - build_dir="$HOME/.arduino/tests/$target/$sketchname/build$i.tmp" + # Calculate build directory for this configuration + if [ "$i" -eq 0 ]; then + build_dir="$build_dir_base" + else + build_dir="${build_dir_base}.${i}" fi + + # Prepare build directory rm -rf "$build_dir" mkdir -p "$build_dir" diff --git a/.github/workflows/docs_build.yml b/.github/workflows/docs_build.yml index 5253c166f85..f80726c9a85 100644 --- a/.github/workflows/docs_build.yml +++ b/.github/workflows/docs_build.yml @@ -5,6 +5,7 @@ on: branches: - master - release/v2.x + - docs-embed paths: - "docs/**" - ".github/workflows/docs_build.yml" @@ -34,18 +35,43 @@ jobs: cache: "pip" python-version: "3.10" - - name: Build + # Install Arduino CLI and set environment variables + - name: Install Arduino CLI + run: | + source .github/scripts/install-arduino-cli.sh + echo "ARDUINO_IDE_PATH=$ARDUINO_IDE_PATH" >> $GITHUB_ENV + echo "ARDUINO_USR_PATH=$ARDUINO_USR_PATH" >> $GITHUB_ENV + + # Install ESP32 Arduino Core + - name: Install ESP32 Arduino Core + run: | + source .github/scripts/install-arduino-core-esp32.sh + + # Install Python dependencies needed for docs_build_examples.py + - name: Install Python Dependencies run: | sudo apt update sudo apt install python3-pip python3-setuptools - # GitHub CI installs pip3 and setuptools outside the path. - # Update the path to include them and run. cd ./docs PATH=/home/runner/.local/bin:$PATH pip3 install -r requirements.txt --prefer-binary + + # Build examples using environment variables from install script + - name: Build Examples + run: | + PATH=/home/runner/.local/bin:$PATH python3 .github/scripts/docs_build_examples.py --build --diagram --launchpad + + - name: Cleanup Binaries + run: | + PATH=/home/runner/.local/bin:$PATH python3 .github/scripts/docs_build_examples.py -c + + + - name: Build + run: | + cd ./docs PATH=/home/runner/.local/bin:$PATH SPHINXOPTS="-W" build-docs -l en - name: Archive Docs uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: docs - path: docs + path: docs \ No newline at end of file diff --git a/.github/workflows/docs_deploy.yml b/.github/workflows/docs_deploy.yml index 01eb2b773dc..2f3274e8a2a 100644 --- a/.github/workflows/docs_deploy.yml +++ b/.github/workflows/docs_deploy.yml @@ -1,6 +1,7 @@ name: Documentation Build and Production Deploy CI on: + workflow_dispatch: workflow_run: workflows: ["ESP32 Arduino Release"] types: @@ -9,6 +10,7 @@ on: branches: - release/v2.x - master + - docs-embed paths: - "docs/**" - ".github/workflows/docs_deploy.yml" @@ -19,7 +21,7 @@ permissions: jobs: deploy-prod-docs: name: Deploy Documentation on Production - if: github.repository == 'espressif/arduino-esp32' + # if: github.repository == 'espressif/arduino-esp32' runs-on: ubuntu-22.04 defaults: run: @@ -41,6 +43,35 @@ jobs: cache: "pip" python-version: "3.10" + # Install Arduino CLI and set environment variables + - name: Install Arduino CLI + run: | + source .github/scripts/install-arduino-cli.sh + echo "ARDUINO_IDE_PATH=$ARDUINO_IDE_PATH" >> $GITHUB_ENV + echo "ARDUINO_USR_PATH=$ARDUINO_USR_PATH" >> $GITHUB_ENV + + # Install ESP32 Arduino Core + - name: Install ESP32 Arduino Core + run: | + source .github/scripts/install-arduino-core-esp32.sh + + # Install Python dependencies needed for docs_build_examples.py + - name: Install Python Dependencies + run: | + sudo apt update + sudo apt install python3-pip python3-setuptools + cd ./docs + PATH=/home/runner/.local/bin:$PATH pip3 install -r requirements.txt --prefer-binary + + # Build examples using environment variables from install script + - name: Build Examples + run: | + PATH=/home/runner/.local/bin:$PATH python3 .github/scripts/docs_build_examples.py --build --diagram --launchpad + + - name: Cleanup Binaries + run: | + PATH=/home/runner/.local/bin:$PATH python3 .github/scripts/docs_build_examples.py -c + - name: Deploy Documentation env: # Deploy to production server diff --git a/docs/conf_common.py b/docs/conf_common.py index 1ef2e5ee2e6..ea1fcb2b5cd 100644 --- a/docs/conf_common.py +++ b/docs/conf_common.py @@ -35,6 +35,7 @@ "sphinx_tabs.tabs", "sphinx_substitution_extensions", # For allowing substitutions inside code blocks "esp_docs.esp_extensions.dummy_build_system", + "esp_docs.generic_extensions.docs_embed", ] # ESP32_DOCS = [ diff --git a/docs/en/api/gpio.rst b/docs/en/api/gpio.rst index ebf31088ffd..0641b96905e 100644 --- a/docs/en/api/gpio.rst +++ b/docs/en/api/gpio.rst @@ -141,25 +141,7 @@ Example Code GPIO Input and Output Modes *************************** -.. code-block:: arduino - - #define LED 12 - #define BUTTON 2 - - uint8_t stateLED = 0; - - void setup() { - pinMode(LED, OUTPUT); - pinMode(BUTTON,INPUT_PULLUP); - } - - void loop() { - - if(!digitalRead(BUTTON)){ - stateLED = stateLED^1; - digitalWrite(LED,stateLED); - } - } +.. wokwi-example:: libraries/ESP32/examples/GPIO/Blink GPIO Interrupt ************** diff --git a/docs/en/conf.py b/docs/en/conf.py index c4291af80e8..81f099f7137 100644 --- a/docs/en/conf.py +++ b/docs/en/conf.py @@ -31,3 +31,10 @@ # Tracking ID for Google Analytics google_analytics_id = "G-F58JM78930" + +# Documentation embedding settings +docs_embed_public_root = "https://docs.espressif.com/projects/arduino-esp32-wokwi-test" +docs_embed_esp32_relative_root = "../.." +docs_embed_launchpad_url = "https://espressif.github.io/esp-launchpad/" +docs_embed_github_base_url = "https://github.com/JakubAndrysek/arduino-esp32" +docs_embed_github_branch = "docs-embed" diff --git a/docs/requirements.txt b/docs/requirements.txt index ef2ab88cb65..3d03ff23596 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,7 +1,9 @@ -sphinx==4.5.0 -esp-docs>=1.4.0 +sphinx~=7.1.2 + +esp-docs @ git+https://github.com/JakubAndrysek/esp-docs.git@extensions/wokwi_embed + sphinx-copybutton==0.5.0 -sphinx-tabs==3.2.0 +sphinx-tabs==3.4.7 numpydoc==1.5.0 standard-imghdr==3.13.0 Sphinx-Substitution-Extensions==2022.2.16 diff --git a/libraries/ESP32/examples/GPIO/Blink/Blink.ino b/libraries/ESP32/examples/GPIO/Blink/Blink.ino new file mode 100644 index 00000000000..dda685ab4d8 --- /dev/null +++ b/libraries/ESP32/examples/GPIO/Blink/Blink.ino @@ -0,0 +1,16 @@ +#define LED 12 +#define BUTTON 2 + +uint8_t stateLED = 0; + +void setup() { + pinMode(LED, OUTPUT); + pinMode(BUTTON, INPUT_PULLUP); +} + +void loop() { + if (!digitalRead(BUTTON)) { + stateLED = !stateLED; + digitalWrite(LED, stateLED); + } +} diff --git a/libraries/ESP32/examples/GPIO/Blink/ci.yml b/libraries/ESP32/examples/GPIO/Blink/ci.yml new file mode 100644 index 00000000000..eb31ec19671 --- /dev/null +++ b/libraries/ESP32/examples/GPIO/Blink/ci.yml @@ -0,0 +1,78 @@ +upload-binary: + targets: + - esp32 + - esp32s3 + diagram: + esp32: + parts: + - type: wokwi-pushbutton + id: btn1 + top: 140.6 + left: 144 + attrs: + color: green + - type: wokwi-led + id: led1 + top: 66 + left: 131.4 + rotate: 90 + attrs: + color: red + connections: + - - btn1:1.l + - esp:0 + - blue + - - h-19.2 + - v-19.2 + - - btn1:2.r + - esp:GND.1 + - black + - - h19.4 + - v48.2 + - h-240 + - v-67.2 + - - esp:GND.2 + - led1:C + - black + - - h33.64 + - v114.8 + - - esp:4 + - led1:A + - green + - - h0 + esp32s3: + parts: + - type: wokwi-pushbutton + id: btn1 + top: 140.6 + left: 115.2 + attrs: + color: green + - type: wokwi-led + id: led1 + top: 66.4 + left: -56.2 + rotate: 270 + attrs: + color: red + flip: "" + connections: + - - btn1:1.l + - esp:0 + - blue + - - h-19.2 + - v0 + - h-4.57 + - - btn1:2.r + - esp:GND.3 + - green + - - h19.4 + - v48.38 + - - esp:4 + - led1:A + - green + - - h0 + - - esp:GND.1 + - led1:C + - black + - - h0 From cb8bc18d8b96e518550f12b4e8ae3b88523e8f67 Mon Sep 17 00:00:00 2001 From: Jakub Andrysek Date: Wed, 29 Oct 2025 13:58:40 +0100 Subject: [PATCH 02/11] feat(docs): Enhance documentation build process with environment variable configuration --- .github/scripts/docs_build_examples.py | 58 +++++++++++++------ .github/workflows/docs_build.yml | 14 ++++- .github/workflows/docs_deploy.yml | 14 ++++- docs/en/conf.py | 10 ++-- libraries/ESP32/examples/GPIO/Blink/Blink.ino | 4 +- 5 files changed, 72 insertions(+), 28 deletions(-) diff --git a/.github/scripts/docs_build_examples.py b/.github/scripts/docs_build_examples.py index 9c3f1a63674..03f149725d8 100755 --- a/.github/scripts/docs_build_examples.py +++ b/.github/scripts/docs_build_examples.py @@ -35,17 +35,34 @@ SCRIPT_DIR = Path(__file__).resolve().parent +env_config_keys = [ + "DOCS_EMBED_PUBLIC_ROOT", + "DOCS_EMBED_GITHUB_BASE_URL", + "DOCS_EMBED_BINARIES_DIR", +] + +# load environment variables +env_config = {} +for key in env_config_keys: + value = os.environ.get(key) + if value is None: + raise EnvironmentError(f"{key} environment variable is not set") + env_config[key] = value + + +if env_config.get("DOCS_EMBED_BINARIES_DIR"): + DOCS_EMBED_BINARIES_PATH = Path(f"docs/{env_config['DOCS_EMBED_BINARIES_DIR']}") + ARDUINO_ESP32_PATH = os.environ.get("ARDUINO_ESP32_PATH") GITHUB_WORKSPACE = os.environ.get("GITHUB_WORKSPACE") -STORAGE_URL_PREFIX = os.environ.get("STORAGE_URL_PREFIX") -REPO_URL_PREFIX = os.environ.get("REPO_URL_PREFIX") if ARDUINO_ESP32_PATH and (Path(ARDUINO_ESP32_PATH) / "tools" / "esp32-arduino-libs").is_dir(): SDKCONFIG_DIR = Path(ARDUINO_ESP32_PATH) / "tools" / "esp32-arduino-libs" elif GITHUB_WORKSPACE and (Path(GITHUB_WORKSPACE) / "tools" / "esp32-arduino-libs").is_dir(): SDKCONFIG_DIR = Path(GITHUB_WORKSPACE) / "tools" / "esp32-arduino-libs" else: - SDKCONFIG_DIR = Path("tools/esp32-arduino-libs") + raise EnvironmentError("Could not locate esp32-arduino-libs directory. " + "Set ARDUINO_ESP32_PATH or GITHUB_WORKSPACE environment variable.") KEEP_FILES = [ "*.merged.bin", @@ -56,8 +73,6 @@ ] SKETCH_UTILS = SCRIPT_DIR / "sketch_utils.sh" -DOCS_BINARIES_DIR = Path("docs/_static/binaries") - def detect_arduino_paths(): """Get Arduino CLI and user paths from environment variables set by install-arduino-cli.sh @@ -269,11 +284,11 @@ def cleanup_binaries(): Removes all files except those matching patterns in KEEP_FILES. Also removes empty directories after cleanup. """ - print(f"Cleaning up binaries directory: {DOCS_BINARIES_DIR}") - if not DOCS_BINARIES_DIR.exists(): + print(f"Cleaning up binaries directory: {DOCS_EMBED_BINARIES_PATH}") + if not DOCS_EMBED_BINARIES_PATH.exists(): print("Binaries directory does not exist, nothing to clean") return - for root, dirs, files in os.walk(DOCS_BINARIES_DIR): + for root, dirs, files in os.walk(DOCS_EMBED_BINARIES_PATH): for fname in files: fpath = Path(root) / fname parent = Path(root).name @@ -292,7 +307,7 @@ def cleanup_binaries(): else: print(f"Keeping: {fpath}") # remove empty dirs - for root, dirs, files in os.walk(DOCS_BINARIES_DIR, topdown=False): + for root, dirs, files in os.walk(DOCS_EMBED_BINARIES_PATH, topdown=False): if not os.listdir(root): try: os.rmdir(root) @@ -356,7 +371,7 @@ def build_example_for_target(sketch_dir, target, relative_path, args): bool: True if build succeeded, False otherwise """ print(f"\n > Building example: {relative_path} for target: {target}") - output_dir = DOCS_BINARIES_DIR / relative_path / target + output_dir = DOCS_EMBED_BINARIES_PATH / relative_path / target output_dir.mkdir(parents=True, exist_ok=True) sdkconfig = SDKCONFIG_DIR / target / 'sdkconfig' @@ -405,13 +420,13 @@ def build_all_examples(args): total_built = 0 total_failed = 0 - if DOCS_BINARIES_DIR.exists(): - shutil.rmtree(DOCS_BINARIES_DIR) - print(f"Removed existing build directory: {DOCS_BINARIES_DIR}") + if DOCS_EMBED_BINARIES_PATH.exists(): + shutil.rmtree(DOCS_EMBED_BINARIES_PATH) + print(f"Removed existing build directory: {DOCS_EMBED_BINARIES_PATH}") # add gitignore to binaries dir with * for new files - DOCS_BINARIES_DIR.mkdir(parents=True, exist_ok=True) - gitignore_path = DOCS_BINARIES_DIR / '.gitignore' + DOCS_EMBED_BINARIES_PATH.mkdir(parents=True, exist_ok=True) + gitignore_path = DOCS_EMBED_BINARIES_PATH / '.gitignore' gitignore_path.write_text("*\n") examples = find_examples_with_upload_binary() @@ -444,7 +459,7 @@ def build_all_examples(args): else: total_failed += 1 - output_sketch_dir = DOCS_BINARIES_DIR / relative_path + output_sketch_dir = DOCS_EMBED_BINARIES_PATH / relative_path output_sketch_dir.mkdir(parents=True, exist_ok=True) # copy sketch ci.yml to output dir - parent of target dirs @@ -455,15 +470,20 @@ def build_all_examples(args): if args.generate_launchpad_config: print(f"Generating LaunchPad config for {relative_path}/{target}...") try: - sync = DiagramSync(output_sketch_dir / target) - sync.generate_launchpad_config(STORAGE_URL_PREFIX, REPO_URL_PREFIX, True, output_sketch_dir) + sync = DiagramSync(output_sketch_dir) + sync.generate_launchpad_config( + env_config['DOCS_EMBED_PUBLIC_ROOT'], + env_config['DOCS_EMBED_GITHUB_BASE_URL'], + True, + output_sketch_dir + ) except Exception as e: print(f"WARNING: Failed to generate LaunchPad config for {relative_path}/{target}: {e}") print('\nBuild summary:') print(f" Successfully built: {total_built}") print(f" Failed builds: {total_failed}") - print(f" Output directory: {DOCS_BINARIES_DIR}") + print(f" Output directory: {DOCS_EMBED_BINARIES_PATH}") return total_failed diff --git a/.github/workflows/docs_build.yml b/.github/workflows/docs_build.yml index f80726c9a85..f0d58a258e0 100644 --- a/.github/workflows/docs_build.yml +++ b/.github/workflows/docs_build.yml @@ -55,7 +55,19 @@ jobs: cd ./docs PATH=/home/runner/.local/bin:$PATH pip3 install -r requirements.txt --prefer-binary - # Build examples using environment variables from install script + # Set documentation embedding environment variables + - name: Set Documentation Environment Variables + run: | + # configure env vars for docs_build_examples.py and docs build + echo "DOCS_EMBED_GITHUB_BASE_URL=https://github.com/${{ github.repository }}" >> $GITHUB_ENV + echo "DOCS_EMBED_GITHUB_BRANCH=${{ github.ref_name }}" >> $GITHUB_ENV + echo "DOCS_EMBED_PUBLIC_ROOT=https://${{ secrets.DOCS_SERVER }}/${{ secrets.DOCS_PATH }}/en/${{ github.ref_name == 'master' && 'latest' || github.ref_name }}/" >> $GITHUB_ENV + echo "DOCS_EMBED_BINARIES_DIR=${{ vars.DOCS_EMBED_BINARIES_DIR }}" >> $GITHUB_ENV + echo "DOCS_EMBED_LAUNCHPAD_URL=${{ vars.DOCS_EMBED_LAUNCHPAD_URL }}" >> $GITHUB_ENV + echo "DOCS_EMBED_WOKWI_VIEWER_URL=${{ vars.DOCS_EMBED_WOKWI_VIEWER_URL }}" >> $GITHUB_ENV + echo "DOCS_EMBED_ABOUT_WOKWI_URL=${{ vars.DOCS_EMBED_ABOUT_WOKWI_URL }}" >> $GITHUB_ENV + + # Build examples using environment variables from install script - name: Build Examples run: | PATH=/home/runner/.local/bin:$PATH python3 .github/scripts/docs_build_examples.py --build --diagram --launchpad diff --git a/.github/workflows/docs_deploy.yml b/.github/workflows/docs_deploy.yml index 2f3274e8a2a..98a58b76486 100644 --- a/.github/workflows/docs_deploy.yml +++ b/.github/workflows/docs_deploy.yml @@ -63,7 +63,19 @@ jobs: cd ./docs PATH=/home/runner/.local/bin:$PATH pip3 install -r requirements.txt --prefer-binary - # Build examples using environment variables from install script + # Set documentation embedding environment variables + - name: Set Documentation Environment Variables + run: | + # configure env vars for docs_build_examples.py and docs deployment + echo "DOCS_EMBED_GITHUB_BASE_URL=https://github.com/${{ github.repository }}" >> $GITHUB_ENV + echo "DOCS_EMBED_GITHUB_BRANCH=${{ github.ref_name }}" >> $GITHUB_ENV + echo "DOCS_EMBED_PUBLIC_ROOT=https://${{ secrets.DOCS_SERVER }}/${{ secrets.DOCS_PATH }}/en/${{ github.ref_name == 'master' && 'latest' || github.ref_name }}/" >> $GITHUB_ENV + echo "DOCS_EMBED_BINARIES_DIR=${{ vars.DOCS_EMBED_BINARIES_DIR }}" >> $GITHUB_ENV + echo "DOCS_EMBED_LAUNCHPAD_URL=${{ vars.DOCS_EMBED_LAUNCHPAD_URL }}" >> $GITHUB_ENV + echo "DOCS_EMBED_WOKWI_VIEWER_URL=${{ vars.DOCS_EMBED_WOKWI_VIEWER_URL }}" >> $GITHUB_ENV + echo "DOCS_EMBED_ABOUT_WOKWI_URL=${{ vars.DOCS_EMBED_ABOUT_WOKWI_URL }}" >> $GITHUB_ENV + + # Build examples using environment variables from install script - name: Build Examples run: | PATH=/home/runner/.local/bin:$PATH python3 .github/scripts/docs_build_examples.py --build --diagram --launchpad diff --git a/docs/en/conf.py b/docs/en/conf.py index 81f099f7137..674ce89f5af 100644 --- a/docs/en/conf.py +++ b/docs/en/conf.py @@ -33,8 +33,8 @@ google_analytics_id = "G-F58JM78930" # Documentation embedding settings -docs_embed_public_root = "https://docs.espressif.com/projects/arduino-esp32-wokwi-test" -docs_embed_esp32_relative_root = "../.." -docs_embed_launchpad_url = "https://espressif.github.io/esp-launchpad/" -docs_embed_github_base_url = "https://github.com/JakubAndrysek/arduino-esp32" -docs_embed_github_branch = "docs-embed" +# docs_embed_public_root = "https://docs.espressif.com/projects/arduino-esp32-wokwi-test" +# docs_embed_esp32_relative_root = "../.." +# docs_embed_launchpad_url = "https://espressif.github.io/esp-launchpad/" +# docs_embed_github_base_url = "https://github.com/JakubAndrysek/arduino-esp32" +# docs_embed_github_branch = "docs-embed" diff --git a/libraries/ESP32/examples/GPIO/Blink/Blink.ino b/libraries/ESP32/examples/GPIO/Blink/Blink.ino index dda685ab4d8..8fb1623343d 100644 --- a/libraries/ESP32/examples/GPIO/Blink/Blink.ino +++ b/libraries/ESP32/examples/GPIO/Blink/Blink.ino @@ -1,5 +1,5 @@ -#define LED 12 -#define BUTTON 2 +#define LED 4 +#define BUTTON 0 uint8_t stateLED = 0; From 8c2635f7e4451962786477a2d3352c1e53864c25 Mon Sep 17 00:00:00 2001 From: Jakub Andrysek Date: Thu, 30 Oct 2025 10:23:39 +0100 Subject: [PATCH 03/11] feat(gpio): Update GPIO examples for clarity and functionality; add interrupt example --- .github/scripts/docs_build_examples.py | 221 +++++++----------- .gitignore | 4 + docs/en/api/gpio.rst | 4 +- libraries/ESP32/examples/GPIO/Blink/Blink.ino | 27 ++- .../GPIO/GPIOInterrupt/GPIOInterrupt.ino | 8 +- .../ESP32/examples/GPIO/GPIOInterrupt/ci.yml | 122 ++++++++++ 6 files changed, 241 insertions(+), 145 deletions(-) create mode 100644 libraries/ESP32/examples/GPIO/GPIOInterrupt/ci.yml diff --git a/.github/scripts/docs_build_examples.py b/.github/scripts/docs_build_examples.py index 03f149725d8..469b8c0882a 100755 --- a/.github/scripts/docs_build_examples.py +++ b/.github/scripts/docs_build_examples.py @@ -8,18 +8,7 @@ The script requires Arduino CLI and user paths when building examples. It processes all sketches that have 'upload-binary' configuration in their -ci.json files and builds them for specified targets. - -Usage: - docs_build_examples.py -c # Clean up - docs_build_examples.py --build -ai /path/cli -au /path/user # Build all - docs_build_examples.py --build -ai /path/cli -au /path/user --diagram --launchpad # Build with extras - -Environment Variables: - ARDUINO_ESP32_PATH: Path to ESP32 Arduino core - GITHUB_WORKSPACE: GitHub workspace path (fallback) - DOCS_PROD_URL_BASE: Base URL for documentation deployment - REPO_URL_PREFIX: Repository URL prefix for LaunchPad configs +ci.yml files and builds them for specified targets. """ import argparse @@ -56,90 +45,46 @@ ARDUINO_ESP32_PATH = os.environ.get("ARDUINO_ESP32_PATH") GITHUB_WORKSPACE = os.environ.get("GITHUB_WORKSPACE") -if ARDUINO_ESP32_PATH and (Path(ARDUINO_ESP32_PATH) / "tools" / "esp32-arduino-libs").is_dir(): +if ( + ARDUINO_ESP32_PATH + and (Path(ARDUINO_ESP32_PATH) / "tools" / "esp32-arduino-libs").is_dir() +): SDKCONFIG_DIR = Path(ARDUINO_ESP32_PATH) / "tools" / "esp32-arduino-libs" -elif GITHUB_WORKSPACE and (Path(GITHUB_WORKSPACE) / "tools" / "esp32-arduino-libs").is_dir(): +elif ( + GITHUB_WORKSPACE + and (Path(GITHUB_WORKSPACE) / "tools" / "esp32-arduino-libs").is_dir() +): SDKCONFIG_DIR = Path(GITHUB_WORKSPACE) / "tools" / "esp32-arduino-libs" else: - raise EnvironmentError("Could not locate esp32-arduino-libs directory. " - "Set ARDUINO_ESP32_PATH or GITHUB_WORKSPACE environment variable.") + raise EnvironmentError( + "Could not locate esp32-arduino-libs directory. " + "Set ARDUINO_ESP32_PATH or GITHUB_WORKSPACE environment variable." + ) KEEP_FILES = [ "*.merged.bin", "ci.yml", "launchpad.toml", "diagram*.json", - ".gitignore", ] SKETCH_UTILS = SCRIPT_DIR / "sketch_utils.sh" -def detect_arduino_paths(): - """Get Arduino CLI and user paths from environment variables set by install-arduino-cli.sh - - The function will create the Arduino user directory if it doesn't exist. - Returns: - tuple: (arduino_cli_path, arduino_user_path) or (None, None) if not found - """ - try: - # Get paths from environment variables exported by install-arduino-cli.sh - arduino_ide_path = os.environ.get("ARDUINO_IDE_PATH") - arduino_usr_path = os.environ.get("ARDUINO_USR_PATH") - - if not arduino_ide_path or not arduino_usr_path: - print("Arduino paths not found in environment variables.") - print("Make sure to run install-arduino-cli.sh first to set ARDUINO_IDE_PATH and ARDUINO_USR_PATH") - return None, None - - # Convert to Path objects for validation - ide_path = Path(arduino_ide_path) - usr_path = Path(arduino_usr_path) - - # Check if arduino-cli exists - arduino_cli_exe = ide_path / "arduino-cli" - if platform.system().lower() == "windows": - arduino_cli_exe = ide_path / "arduino-cli.exe" - - if arduino_cli_exe.exists(): - # Create user path if it doesn't exist - if not usr_path.exists(): - try: - usr_path.mkdir(parents=True, exist_ok=True) - print(f"Created Arduino user directory: {usr_path}") - except Exception as e: - print(f"Failed to create Arduino user directory {usr_path}: {e}") - return None, None - - return str(ide_path), str(usr_path) - else: - print(f"Arduino CLI or user path not found:") - print(f" Arduino CLI: {arduino_cli_exe} {'✓' if arduino_cli_exe.exists() else '✗'}") - print(f" User path: {usr_path} {'✓' if usr_path.exists() else '✗'}") - return None, None - - except Exception as e: - print(f"Error getting Arduino paths from environment: {e}") - return None, None def run_cmd(cmd, check=True, capture_output=False, text=True): - """Execute a shell command with error handling. - - Args: - cmd (list): Command and arguments to execute - check (bool): Whether to raise exception on non-zero exit code - capture_output (bool): Whether to capture stdout/stderr - text (bool): Whether to return text output instead of bytes - """ + """Execute a shell command with error handling.""" try: - return subprocess.run(cmd, check=check, capture_output=capture_output, text=text) + return subprocess.run( + cmd, check=check, capture_output=capture_output, text=text + ) except subprocess.CalledProcessError as e: # CalledProcessError is raised only when check=True and the command exits non-zero print(f"ERROR: Command failed: {' '.join(cmd)}") print(f"Exit code: {e.returncode}") - if hasattr(e, 'stdout') and e.stdout: + if hasattr(e, "stdout") and e.stdout: print("--- stdout ---") print(e.stdout) - if hasattr(e, 'stderr') and e.stderr: + if hasattr(e, "stderr") and e.stderr: print("--- stderr ---") print(e.stderr) # Exit the whole script with the same return code to mimic shell behavior @@ -183,12 +128,13 @@ def parse_args(argv): """Parse command line arguments""" epilog_text = ( "Examples:\n" - " docs_build_examples.py -c # Clean up binaries directory\n" - " docs_build_examples.py --build # Build all examples (use env vars)\n" - " docs_build_examples.py --build -ai /path/to/cli -au /path/to/user # Build with explicit paths\n" - " docs_build_examples.py --build --diagram --launchpad # Build with diagrams and LaunchPad\n\n" + " docs_build_examples.py -c # Clean up binaries directory\n" + " docs_build_examples.py --build # Build all examples (use env vars)\n" + " docs_build_examples.py --build -ai /path/to/cli -au /path/to/user # Build with explicit paths\n" + " docs_build_examples.py --build --diagram --launchpad # Build with diagrams and LaunchPad (with env vars)\n\n" "Path detection:\n" - " Arduino paths are read from ARDUINO_IDE_PATH and ARDUINO_USR_PATH environment variables\n" + " All paths can be set via environment variables or command line options.\n" + " ARDUINO_IDE_PATH and ARDUINO_USR_PATH environment variables are used by default.\n" " Set by running install-arduino-cli.sh first, or use -ai/-au to override\n\n" ) @@ -198,35 +144,41 @@ def parse_args(argv): epilog=epilog_text, ) p.add_argument( - "-c", "--cleanup", + "-c", + "--cleanup", dest="cleanup", action="store_true", help="Clean up docs binaries directory and exit", ) p.add_argument( - "-ai", "--arduino-cli-path", + "-ai", + "--arduino-cli-path", dest="arduino_cli_path", help="Path to Arduino CLI installation directory (overrides ARDUINO_IDE_PATH env var)", ) p.add_argument( - "-au", "--arduino-user-path", + "-au", + "--arduino-user-path", dest="user_path", help="Path to Arduino user directory (overrides ARDUINO_USR_PATH env var)", ) p.add_argument( - "-b", "--build", + "-b", + "--build", dest="build", action="store_true", help="Build all examples", ) p.add_argument( - "-d", "--diagram", + "-d", + "--diagram", dest="generate_diagrams", action="store_true", help="Generate diagrams for prepared examples using docs-embed", ) p.add_argument( - "-l", "--launchpad", + "-l", + "--launchpad", dest="generate_launchpad_config", action="store_true", help="Generate LaunchPad config for prepared examples", @@ -240,35 +192,31 @@ def validate_prerequisites(args): # Get paths from environment variables if not provided via arguments if not args.arduino_cli_path or not args.user_path: print("Getting Arduino paths from environment variables...") - detected_cli_path, detected_user_path = detect_arduino_paths() + arduino_ide_path = os.environ.get("ARDUINO_IDE_PATH") + arduino_usr_path = os.environ.get("ARDUINO_USR_PATH") if not args.arduino_cli_path: - if detected_cli_path: - args.arduino_cli_path = detected_cli_path - print(f" Arduino CLI path (ARDUINO_IDE_PATH): {detected_cli_path}") + if arduino_ide_path: + args.arduino_cli_path = arduino_ide_path + print(f" Arduino CLI path (ARDUINO_IDE_PATH): {arduino_ide_path}") else: - print("ERROR: Arduino CLI path not found in ARDUINO_IDE_PATH env var and not provided (-ai option)") + print( + "ERROR: Arduino CLI path not found in ARDUINO_IDE_PATH env var and not provided (-ai option)" + ) print("Run install-arduino-cli.sh first to set environment variables") sys.exit(1) if not args.user_path: - if detected_user_path: - args.user_path = detected_user_path - print(f" Arduino user path (ARDUINO_USR_PATH): {detected_user_path}") + if arduino_usr_path: + args.user_path = arduino_usr_path + print(f" Arduino user path (ARDUINO_USR_PATH): {arduino_usr_path}") else: - print("ERROR: Arduino user path not found in ARDUINO_USR_PATH env var and not provided (-au option)") + print( + "ERROR: Arduino user path not found in ARDUINO_USR_PATH env var and not provided (-au option)" + ) print("Run install-arduino-cli.sh first to set environment variables") sys.exit(1) - # Validate the paths - arduino_cli_exe = Path(args.arduino_cli_path) / "arduino-cli" - if platform.system().lower() == "windows": - arduino_cli_exe = Path(args.arduino_cli_path) / "arduino-cli.exe" - - if not arduino_cli_exe.exists(): - print(f"ERROR: arduino-cli not found at {arduino_cli_exe}") - sys.exit(1) - # Create Arduino user path if it doesn't exist user_path = Path(args.user_path) if not user_path.is_dir(): @@ -278,6 +226,8 @@ def validate_prerequisites(args): except Exception as e: print(f"ERROR: Failed to create Arduino user path {user_path}: {e}") sys.exit(1) + + def cleanup_binaries(): """Clean up the binaries directory, keeping only specified files. @@ -323,17 +273,17 @@ def find_examples_with_upload_binary(): list: List of paths to .ino files that have upload-binary in ci.yml """ res = [] - for ino in Path('.').rglob('*.ino'): + for ino in Path(".").rglob("*.ino"): sketch_dir = ino.parent sketch_name = ino.stem dir_name = sketch_dir.name if dir_name != sketch_name: continue - ci_yml = sketch_dir / 'ci.yml' + ci_yml = sketch_dir / "ci.yml" if ci_yml.exists(): try: data = yaml.safe_load(ci_yml.read_text()) - if 'upload-binary' in data and data['upload-binary']: + if "upload-binary" in data and data["upload-binary"]: res.append(str(ino)) except Exception: continue @@ -349,10 +299,10 @@ def get_upload_binary_targets(sketch_dir): Returns: list: List of target names for upload-binary, empty if none found """ - ci_yml = Path(sketch_dir) / 'ci.yml' + ci_yml = Path(sketch_dir) / "ci.yml" try: data = yaml.safe_load(ci_yml.read_text()) - targets = data.get('upload-binary', {}).get('targets', []) + targets = data.get("upload-binary", {}).get("targets", []) return targets except Exception: return [] @@ -374,9 +324,11 @@ def build_example_for_target(sketch_dir, target, relative_path, args): output_dir = DOCS_EMBED_BINARIES_PATH / relative_path / target output_dir.mkdir(parents=True, exist_ok=True) - sdkconfig = SDKCONFIG_DIR / target / 'sdkconfig' + sdkconfig = SDKCONFIG_DIR / target / "sdkconfig" if not check_requirements(str(sketch_dir), sdkconfig): - print(f"Target {target} does not meet the requirements for {Path(sketch_dir).name}. Skipping.") + print( + f"Target {target} does not meet the requirements for {Path(sketch_dir).name}. Skipping." + ) return True # Build the sketch using sketch_utils.sh build - pass args as in shell script @@ -396,16 +348,18 @@ def build_example_for_target(sketch_dir, target, relative_path, args): res = build_sketch(build_args) if res.returncode == 0: print(f"Successfully built {relative_path} for {target}") - ci_yml = Path(sketch_dir) / 'ci.yml' + ci_yml = Path(sketch_dir) / "ci.yml" if ci_yml.exists(): - shutil.copy(ci_yml, output_dir / 'ci.yml') + shutil.copy(ci_yml, output_dir / "ci.yml") if args.generate_diagrams: print(f"Generating diagram for {relative_path} ({target})...") try: sync = DiagramSync(output_dir) sync.generate_diagram_from_ci(target) except Exception as e: - print(f"WARNING: Failed to generate diagram for {relative_path} ({target}): {e}") + print( + f"WARNING: Failed to generate diagram for {relative_path} ({target}): {e}" + ) else: print(f"ERROR: Failed to build {relative_path} for {target}") return False @@ -426,7 +380,7 @@ def build_all_examples(args): # add gitignore to binaries dir with * for new files DOCS_EMBED_BINARIES_PATH.mkdir(parents=True, exist_ok=True) - gitignore_path = DOCS_EMBED_BINARIES_PATH / '.gitignore' + gitignore_path = DOCS_EMBED_BINARIES_PATH / ".gitignore" gitignore_path.write_text("*\n") examples = find_examples_with_upload_binary() @@ -434,11 +388,11 @@ def build_all_examples(args): print("No examples found with upload-binary configuration") return 0 - print('\nExamples to be built:') - print('====================') + print("\nExamples to be built:") + print("====================") for i, example in enumerate(examples, start=1): sketch_dir = Path(example).parent - relative_path = str(sketch_dir).lstrip('./') + relative_path = str(sketch_dir).lstrip("./") targets = get_upload_binary_targets(sketch_dir) if targets: print(f"{i}. {relative_path} (targets: {' '.join(targets)})") @@ -446,15 +400,14 @@ def build_all_examples(args): for example in examples: sketch_dir = Path(example).parent - relative_path = str(sketch_dir).lstrip('./') + relative_path = str(sketch_dir).lstrip("./") targets = get_upload_binary_targets(sketch_dir) if not targets: print(f"WARNING: No targets found for {relative_path}") continue print(f"Building {relative_path} for targets: {targets}") for target in targets: - ok = build_example_for_target(sketch_dir, target, relative_path, args) - if ok is not False: # Handle None return as success + if build_example_for_target(sketch_dir, target, relative_path, args): total_built += 1 else: total_failed += 1 @@ -463,24 +416,26 @@ def build_all_examples(args): output_sketch_dir.mkdir(parents=True, exist_ok=True) # copy sketch ci.yml to output dir - parent of target dirs - ci_yml = sketch_dir / 'ci.yml' + ci_yml = sketch_dir / "ci.yml" if ci_yml.exists(): - shutil.copy(ci_yml, output_sketch_dir / 'ci.yml') + shutil.copy(ci_yml, output_sketch_dir / "ci.yml") if args.generate_launchpad_config: print(f"Generating LaunchPad config for {relative_path}/{target}...") try: sync = DiagramSync(output_sketch_dir) sync.generate_launchpad_config( - env_config['DOCS_EMBED_PUBLIC_ROOT'], - env_config['DOCS_EMBED_GITHUB_BASE_URL'], + env_config["DOCS_EMBED_PUBLIC_ROOT"], + env_config["DOCS_EMBED_GITHUB_BASE_URL"], True, - output_sketch_dir + output_sketch_dir, ) except Exception as e: - print(f"WARNING: Failed to generate LaunchPad config for {relative_path}/{target}: {e}") + print( + f"WARNING: Failed to generate LaunchPad config for {relative_path}/{target}: {e}" + ) - print('\nBuild summary:') + print("\nBuild summary:") print(f" Successfully built: {total_built}") print(f" Failed builds: {total_failed}") print(f" Output directory: {DOCS_EMBED_BINARIES_PATH}") @@ -501,19 +456,21 @@ def main(argv): result = build_all_examples(args) if result == 0: - print('\nAll examples built successfully!') + print("\nAll examples built successfully!") else: - print('\nSome builds failed. Check the output above for details.') + print("\nSome builds failed. Check the output above for details.") sys.exit(1) return if args.generate_diagrams or args.generate_launchpad_config: - print("ERROR: --diagram and --launchpad options are only available when building examples (--build)") + print( + "ERROR: --diagram and --launchpad options are only available when building examples (--build)" + ) sys.exit(1) # If no specific action is requested, show help - parse_args(['--help']) + parse_args(["--help"]) -if __name__ == '__main__': +if __name__ == "__main__": main(sys.argv[1:]) diff --git a/.gitignore b/.gitignore index 66b8f1b8031..84312d174c8 100644 --- a/.gitignore +++ b/.gitignore @@ -62,3 +62,7 @@ libraries/Insights/examples/*/*.ino.zip /affected_sketches.txt /affected_examples.txt /ctags_* + +# Disallow Wokwi diagram files except for diagram in tests folder +**/diagram.*.json +!tests/**/diagram.*.json diff --git a/docs/en/api/gpio.rst b/docs/en/api/gpio.rst index 0641b96905e..a7a9779aa4c 100644 --- a/docs/en/api/gpio.rst +++ b/docs/en/api/gpio.rst @@ -146,7 +146,7 @@ GPIO Input and Output Modes GPIO Interrupt ************** -.. literalinclude:: ../../../libraries/ESP32/examples/GPIO/GPIOInterrupt/GPIOInterrupt.ino - :language: arduino +.. wokwi-example:: libraries/ESP32/examples/GPIO/GPIOInterrupt + .. _datasheet: https://www.espressif.com/sites/default/files/documentation/esp32_datasheet_en.pdf diff --git a/libraries/ESP32/examples/GPIO/Blink/Blink.ino b/libraries/ESP32/examples/GPIO/Blink/Blink.ino index 8fb1623343d..037e363ddc3 100644 --- a/libraries/ESP32/examples/GPIO/Blink/Blink.ino +++ b/libraries/ESP32/examples/GPIO/Blink/Blink.ino @@ -1,16 +1,25 @@ -#define LED 4 -#define BUTTON 0 - -uint8_t stateLED = 0; +// Define pin numbers +#define LED_PIN 4 +#define BUTTON_PIN 0 void setup() { - pinMode(LED, OUTPUT); - pinMode(BUTTON, INPUT_PULLUP); + // Initialize the LED pin as an output + pinMode(LED_PIN, OUTPUT); + + // Initialize the button pin as an input with internal pull-up resistor + pinMode(BUTTON_PIN, INPUT_PULLUP); } void loop() { - if (!digitalRead(BUTTON)) { - stateLED = !stateLED; - digitalWrite(LED, stateLED); + // Check if button is pressed (LOW because of INPUT_PULLUP) + if (digitalRead(BUTTON_PIN) == LOW) { + // Blink LED while button is held down + digitalWrite(LED_PIN, HIGH); // Turn LED on + delay(100); // Wait 100ms + digitalWrite(LED_PIN, LOW); // Turn LED off + delay(100); // Wait 100ms + } else { + // Keep LED off when button is not pressed + digitalWrite(LED_PIN, LOW); } } diff --git a/libraries/ESP32/examples/GPIO/GPIOInterrupt/GPIOInterrupt.ino b/libraries/ESP32/examples/GPIO/GPIOInterrupt/GPIOInterrupt.ino index 2e94176224e..b9435d559f2 100644 --- a/libraries/ESP32/examples/GPIO/GPIOInterrupt/GPIOInterrupt.ino +++ b/libraries/ESP32/examples/GPIO/GPIOInterrupt/GPIOInterrupt.ino @@ -6,8 +6,8 @@ struct Button { bool pressed; }; -Button button1 = {23, 0, false}; -Button button2 = {18, 0, false}; +Button button1 = {0, 0, false}; +Button button2 = {4, 0, false}; void ARDUINO_ISR_ATTR isr(void *arg) { Button *s = static_cast