Skill: 547895019/esp-idf-helper (version 1.1.0)

--- name: esp-idf-helper description: Help develop, build, flash, and debug ESP32/ESP8266 firmware using Espressif ESP-IDF on Linux/WSL. Use when the user asks about ESP-IDF project setup, configuring targets, menuconfig, building, flashing via esptool/idf.py, serial monitor, partition tables, sdkconfig, troubleshooting build/flash/monitor errors, or automating common idf.py workflows from the command line. homepage: https://github.com/espressif/esp-idf metadata: { "esp-idf-helper": { "emoji": "🤖", "requires": { "bins": ["idf.py"], "env": ["IDF_PATH"] }, "primaryEnv": "IDF_PATH", }, } --- # esp-idf-helper Skill Provide a repeatable, command-line-first workflow for ESP-IDF development on Linux/WSL: configure → build → flash → monitor → debug/troubleshoot. ## Quick Reference ```bash # 1) Source the ESP-IDF environment (once per terminal session) . $IDF_PATH/export.sh # 1.1) Enable ccache to speed up compilation (recommended) export IDF_CCACHE_ENABLE=1 # 2) Go to your project and build cd /path/to/your/project idf.py set-target <target> # Set target chip (once per project) idf.py build # Compile # 3) flash idf.py -p <PORT> -b <BAUD> flash # Flash to device (optional) ``` ### Common commands - `idf.py --help` — Help - `idf.py set-target <target>` — Set chip target: esp32, esp32s2, esp32s3, esp32c3, esp32p4 - `idf.py menuconfig` — Configure project settings (**must run in a new terminal window**) - `idf.py build` — Build the project - `idf.py update-dependencies` — Update project component dependencies - `idf.py partition-table` — Build partition table and print partition entries - `idf.py partition-table-flash` — Flash partition table to device - `idf.py storage-flash` — Flash storage filesystem partition - `idf.py size` — Show firmware size information - `idf.py -p <PORT> -b <BAUD> flash` — Flash firmware (default baud: 460800) - `idf.py -p <PORT> monitor` — Open serial monitor - `idf.py -p <PORT> -b <BAUD> monitor` — Open serial monitor with specific baud (e.g. 460800) - `idf.py -p <PORT> -b <BAUD> flash monitor` — Flash then monitor ## Component Management ESP-IDF projects can include external components from the **ESP Component Registry**. - **Registry Website:** https://components.espressif.com/components - **Search components:** Browse or search for components on the registry website ### Component Commands - `idf.py add-dependency "<component>"` — Add a component dependency to `idf_component.yml` - `idf.py update-dependencies` — Download and update all project dependencies ### Component Management Workflow ```bash # 1) Add a dependency to your project idf.py add-dependency "<component>" # 2) Update dependencies (downloads components to managed_components/) idf.py update-dependencies ``` **Note:** Dependencies are recorded in `idf_component.yml` in your project's main component directory (`main/`). ## Bundled resources ### references/ - `references/esp-idf-cli.md` — concise command patterns + what to paste back when reporting errors. - `references/idf-py-help.txt` — captured `idf.py --help` output for quick lookup/search. To refresh the help text for your installed ESP-IDF version, run: - `scripts/capture_idf_help.sh` ### assets/ Not used by default. ## Serial Port Management (WSL2) For WSL2 users, USB serial devices need to be attached via **usbipd** to be accessible in WSL. ### List Available Serial Devices ```bash scripts/usbipd_attach_serial.sh --list ``` Shows all connected USB serial devices (CH340, CH343, CP210, FTDI, etc.). **Note:** This script runs in WSL2 and uses `powershell.exe` to communicate with Windows usbipd. ### Bind/Attach All Serial Devices ```bash # Bind and attach all COM port devices scripts/usbipd_attach_serial.sh --keyword "COM" # Or attach specific device by busid scripts/usbipd_attach_serial.sh --busid 3-2 # Or filter by device type scripts/usbipd_attach_serial.sh --keyword "CH343" scripts/usbipd_attach_serial.sh --keyword "ESP32" ``` ### Serial Port Script Options - `--list` — List all matching serial devices and exit - `--busid <BUSID>` — Specify device bus ID (e.g., `3-2`) - `--keyword <TEXT>` — Filter devices by keyword (e.g., `COM`, `CH343`, `ESP32`) - `--bind` — Bind only (skip attach), useful for first-time setup with admin privileges - `--distro <DISTRO>` — Specify WSL distribution name - `--dry-run` — Print commands without executing ### Typical Workflow ```bash # 1. Check available devices scripts/usbipd_attach_serial.sh --list # 2. Attach all serial devices scripts/usbipd_attach_serial.sh --keyword "COM" # 3. Verify devices in WSL ls -la /dev/ttyACM* /dev/ttyUSB* # 4. Use with idf.py idf.py -p /dev/ttyACM0 flash monitor ``` **Note:** This script runs in WSL2 and internally uses `powershell.exe` to communicate with Windows usbipd service. ### Troubleshooting: powershell.exe not found If you encounter `powershell.exe: command not found`, the Windows paths are not in your PATH environment variable. **Quick Fix (current session only):** ```bash export PATH="$PATH:/mnt/c/Windows/System32:/mnt/c/Windows/System32/WindowsPowerShell/v1.0" ~/skills/esp-idf-helper/scripts/usbipd_attach_serial.sh --list ``` **Permanent Fix (add to ~/.bashrc):** ```bash echo 'export PATH="$PATH:/mnt/c/Windows/System32:/mnt/c/Windows/System32/WindowsPowerShell/v1.0:/mnt/c/Windows/SysWOW64"' >> ~/.bashrc source ~/.bashrc ``` ## Firmware Packaging Pack ESP-IDF build output into a distributable firmware package with cross-platform flash scripts. ### Usage ```bash scripts/pack_firmware.sh <build_directory> ``` ### Example ```bash # After building your project idf.py build # Create firmware package scripts/pack_firmware.sh ./build # Output: build/firmware_package/ and build/esp_firmware_YYYYMMDD_HHMMSS.zip ``` ### Generated Package Contents | File | Description | |------|-------------| | `flash.sh` | Linux/Mac flash script with retry and parallel support | | `flash.bat` | Windows multi-port flash launcher | | `flash_one.bat` | Windows single-port flash with retry | | `mac_addresses.txt` | Recorded MAC addresses (deduplicated) | | `*.bin` | Firmware binary files | | `tools/esptool/esptool.exe` | Windows esptool executable | | `README.txt` | Usage instructions | ### Flash Script Features - **Auto-retry**: 3 attempts on failure - **Parallel flashing**: Multiple devices simultaneously - **MAC recording**: Automatic MAC address extraction and deduplication - **Cross-platform**: Linux/Mac/Windows support ### Production Workflow ```bash # 1. Build the project idf.py build # 2. Package firmware scripts/pack_firmware.sh ./build # 3. Distribute the ZIP to production line # Production team runs: flash.bat all (Windows) or ./flash.sh /dev/ttyUSB* (Linux) ```

path: references/idf-py-help.txt
content: Usage: idf.py [OPTIONS] COMMAND1 [ARGS]... [COMMAND2 [ARGS]...]...

  ESP-IDF CLI build management tool. For commands that are not known to idf.py
  an attempt to execute it as a build system target will be made. Selected
  target: None

Options:
  --version                       Show IDF version and exit.
  --list-targets                  Print list of supported targets and exit.
  -C, --project-dir PATH          Project directory.
  -B, --build-dir PATH            Build directory.
  -w, --cmake-warn-uninitialized / -n, --no-warnings
                                  Enable CMake uninitialized variable warnings
                                  for CMake files inside the project
                                  directory. (--no-warnings is now the
                                  default, and doesn't need to be specified.)
                                  The default value can be set with the
                                  IDF_CMAKE_WARN_UNINITIALIZED environment
                                  variable.
  -v, --verbose                   Verbose build output.
  --preview                       Enable IDF features that are still in
                                  preview.
  --ccache / --no-ccache          Use ccache in build. Disabled by default.
                                  The default value can be set with the
                                  IDF_CCACHE_ENABLE environment variable.
  -G, --generator [Ninja|Unix Makefiles]
                                  CMake generator.
  --no-hints                      Disable hints on how to resolve errors and
                                  logging.
  --local-storage-url TEXT        Mirror URLs. To set a list of values, use
                                  this argument multiple times: --local-
                                  storage-url <url1> --local-storage-url
                                  <url2>. IDF_COMPONENT_LOCAL_STORAGE_URL
                                  environment variable can be used to set the
                                  default value.
  -D, --define-cache-entry TEXT   Create a cmake cache entry. This option can
                                  be used at most once either globally, or for
                                  one subcommand.
  -p, --port PATH                 Serial port. The default value can be set
                                  with the ESPPORT environment variable. This
                                  option can be used at most once either
                                  globally, or for one subcommand.
  -b, --baud INTEGER              Global baud rate for all idf.py subcommands
                                  if they don't overwrite it locally.It can
                                  imply monitor baud rate as well if it hasn't
                                  been defined locally. The default value can
                                  be set with the ESPBAUD environment
                                  variable. This option can be used at most
                                  once either globally, or for one subcommand.
  --profile, --service-profile TEXT
                                  Specifies the profile to use for this
                                  command. By default profile named "default"
                                  will be used. Alias "--service-profile" is
                                  deprecated and will be removed. The default
                                  value can be set with the
                                  IDF_COMPONENT_PROFILE environment variable.
                                  This option can be used at most once either
                                  globally, or for one subcommand.
  --registry-url TEXT             URL of the Component Registry.
                                  IDF_COMPONENT_REGISTRY_URL environment
                                  variable can be used to set the default
                                  value. This option can be used at most once
                                  either globally, or for one subcommand.
  --storage-url TEXT              Storage URLs. To set a list of values, use
                                  this argument multiple times: --storage-url
                                  <url1> --storage-url <url2>.
                                  IDF_COMPONENT_STORAGE_URL environment
                                  variable can be used to set the default
                                  value. This option can be used at most once
                                  either globally, or for one subcommand.
  --help                          Show this message and exit.

Commands:
  add-dependency                  Add dependency to the manifest file.
  all                             Aliases: build. Build the project.
  app                             Build only the app.
  app-flash                       Flash the app only.
  bootloader                      Build only bootloader.
  bootloader-flash                Flash bootloader only.
  build-system-targets            Print list of build system targets.
  clang-check                     run clang-tidy check under current folder,
                                  write the output into "warnings.txt"
  clang-html-report               generate html report to "html_report" folder
                                  by reading "warnings.txt" (may take a few
                                  minutes). This feature requires extra
                                  dependency "codereport". Please install this
                                  by running "pip install codereport"
  clean                           Delete build output files from the build
                                  directory.
  confserver                      Run JSON configuration server.
  coredump-debug                  Create core dump ELF file and run GDB debug
                                  session with this file.
  coredump-info                   Print crashed task’s registers, callstack,
                                  list of available tasks in the system,
                                  memory regions and contents of memory stored
                                  in core dump (TCBs and stacks)
  create-component                Create a new component.
  create-manifest                 Create manifest for specified component.
  create-project                  Create a new project.
  create-project-from-example     Create a project from an example...
  diag                            Create diagnostic report.
  docs                            Open web browser with documentation for ESP-
                                  IDF
  efuse-burn                      Burn the eFuse with the specified name.
  efuse-burn-key                  Burn a 256-bit key to EFUSE: BLOCK1,
                                  flash_encryption, BLOCK2, secure_boot_v1,
                                  secure_boot_v2, BLOCK3.
  efuse-common-table              Generate C-source for IDF's eFuse fields.
  efuse-custom-table              Generate C-source for user's eFuse fields.
  efuse-dump                      Dump raw hex values of all eFuses.
  efuse-read-protect              Disable writing to the eFuse with the
                                  specified name.
  efuse-summary                   Get the summary of the eFuses.
  efuse-write-protect             Disable writing to the eFuse with the
                                  specified name.
  encrypted-app-flash             Flash the encrypted app only.
  encrypted-flash                 Flash the encrypted project.
  erase-flash                     Erase entire flash chip.
  erase-otadata                   Erase otadata partition.
  flash                           Flash the project.
  fullclean                       Delete the entire build directory contents.
  gdb                             Run the GDB.
  gdbgui                          GDB UI in default browser.
  gdbtui                          GDB TUI mode.
  menuconfig                      Run "menuconfig" project configuration tool.
  merge-bin
  monitor                         Display serial output.
  openocd                         Run openocd from current path
  partition-table                 Build only partition table.
  partition-table-flash           Flash partition table only.
  post-debug                      Utility target to read the output of async
                                  debug action and stop them.
  python-clean                    Delete generated Python byte code from the
                                  IDF directory
  qemu                            Run QEMU.
  read-otadata                    Read otadata partition.
  reconfigure                     Re-run CMake.
  save-defconfig                  Generate a sdkconfig.defaults with options
                                  different from the default ones
  secure-decrypt-flash-data
  secure-digest-secure-bootloader
                                  Take a bootloader binary image and a secure
                                  boot key, and output a combineddigest+binary
                                  suitable for flashing along with the
                                  precalculated secure boot key.
  secure-encrypt-flash-data       Encrypt some data suitable for encrypted
                                  flash (using known key).
  secure-encrypt-nvs-partition    Encrypt the NVS partition.
  secure-generate-flash-encryption-key
  secure-generate-key-digest      Generate a digest of a puiblic key file for
                                  use with secure boot.
  secure-generate-nvs-partition-key
                                  Generate a key for NVS partition encryption.
  secure-generate-signing-key     Generate a private key for signing secure
                                  boot images as per the secure boot version.
                                  Key file is generated in PEM format, Secure
                                  Boot V1 - ECDSA NIST256p private key. Secure
                                  Boot V2 - RSA 3072, ECDSA NIST256p, ECDSA
                                  NIST192p private key.
  secure-sign-data                Sign a data file for use with secure boot.
                                  Signing algorithm is deterministic ECDSA w/
                                  SHA-512 (V1) or either RSA-PSS or ECDSA w/
                                  SHA-256 (V2).
  secure-verify-signature         Verify a previously signed binary image,
                                  using the ECDSA (V1) or either RSA or ECDSA
                                  (V2) public key.
  set-target                      Set the chip target to build.
  show-efuse-table                Print eFuse table.
  size                            Print basic size information about the app.
  size-components                 Print per-component size information.
  size-files                      Print per-source-file size information.
  uf2                             Generate the UF2 binary with all the
                                  binaries included
  uf2-app                         Generate an UF2 binary for the application
                                  only
  update-dependencies             Update dependencies of the project


path: scripts/capture_idf_help.sh
content: #!/usr/bin/env bash
set -euo pipefail

# Capture idf.py help text into references/ so it can be searched quickly.
# Uses IDF_PATH to activate the environment if available.

OUT_REL="references/idf-py-help.txt"

ESPIDF_ROOT="${IDF_PATH:-}"

cd "$(dirname "$0")/.."  # skill root

if [[ -n "${IDF_PATH:-}" && -f "$ESPIDF_ROOT/export.sh" ]]; then
  bash -lc "set -euo pipefail; source \"$ESPIDF_ROOT/export.sh\" >/dev/null; idf.py --help" > "$OUT_REL"
elif command -v idf.py >/dev/null 2>&1; then
  idf.py --help > "$OUT_REL"
else
  echo "ERROR: idf.py not found. Either activate ESP-IDF in this shell, or set IDF_PATH to an ESP-IDF checkout (with export.sh)." >&2
  exit 1
fi

echo "Wrote: $(pwd)/$OUT_REL"


path: scripts/pack_firmware.sh
content: #!/bin/bash
set -e

if [ $# -lt 1 ]; then
    echo "ERROR: Please provide build directory path"
    echo "Usage: $0 <build_directory>"
    exit 1
fi

BUILD_DIR="$1"
if [[ "$BUILD_DIR" != /* ]]; then
    BUILD_DIR="$(cd "$(dirname "$0")" && cd "$BUILD_DIR" && pwd)"
else
    BUILD_DIR="$(cd "$BUILD_DIR" && pwd)"
fi

FLASH_ARGS_FILE="${BUILD_DIR}/flash_args"
OUTPUT_DIR="${BUILD_DIR}/firmware_package"
ZIP_NAME="esp_firmware_$(date +%Y%m%d_%H%M%S).zip"
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
ESPTOOL_WIN_DIR="tools/esptool"
ESPTOOL_SOURCE="${SCRIPT_DIR}/esptool/esptool.exe"

if [ ! -d "${BUILD_DIR}" ]; then
    echo "ERROR: Build directory not found: ${BUILD_DIR}"
    exit 1
fi

if [ ! -f "${FLASH_ARGS_FILE}" ]; then
    echo "ERROR: flash_args file not found: ${FLASH_ARGS_FILE}"
    exit 1
fi

mkdir -p "${OUTPUT_DIR}"

echo "Build directory: ${BUILD_DIR}"
echo "Parsing flash_args..."

FLASH_PARAMS=$(head -1 "${FLASH_ARGS_FILE}")

FIRMWARE_LIST=""
WIN_FIRMWARE_ARGS=""
while read -r line; do
    [ -z "$line" ] && continue
    addr=$(echo "$line" | awk '{print $1}')
    file=$(echo "$line" | awk '{print $2}')
    filename=$(basename "$file")
    FIRMWARE_LIST="${FIRMWARE_LIST}        ${addr} ${filename} \\\n"
    WIN_FIRMWARE_ARGS="${WIN_FIRMWARE_ARGS} ${addr} ${filename}"
done < <(tail -n +2 "${FLASH_ARGS_FILE}")

echo "Generating flash scripts..."

cat > "${OUTPUT_DIR}/flash.sh" << SCRIPT_EOF
#!/bin/bash
set -e
BAUD="460800"
CHIP="esp32s2"
ESPTOOL="esptool.py"
MAC_RECORD_FILE="mac_addresses.txt"
READ_MAC_ONLY=0

while [[ "$1" == "--read-mac" ]]; do
    READ_MAC_ONLY=1
    shift
done

record_mac() {
    local PORT=\$1
    local DEVICE_NAME=\$(basename "\$PORT")
    local MAC_LOG="/tmp/read_mac_\${DEVICE_NAME}.log"
    local MAC_CLEAN
    if ! \${ESPTOOL} --chip \${CHIP} -p \${PORT} -b \${BAUD} read_mac > "\${MAC_LOG}" 2>&1; then
        echo "[\${DEVICE_NAME}] Warning: failed to read MAC" >&2
        return 1
    fi
    MAC_CLEAN=\$(grep -Eo '([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}' "\${MAC_LOG}" | head -n 1 | tr -d ':' | tr '[:upper:]' '[:lower:]')
    [ -z "\${MAC_CLEAN}" ] && return 1
    touch "\${MAC_RECORD_FILE}"
    if ! grep -Fxq "\${MAC_CLEAN}" "\${MAC_RECORD_FILE}"; then
        echo "\${MAC_CLEAN}" >> "\${MAC_RECORD_FILE}"
    fi
}

record_mac_from_log() {
    local LOG_FILE=\$1
    local DEVICE_NAME=\$2
    local MAC_RAW
    local MAC_CLEAN

    MAC_RAW=\$(grep -Eo '([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}' "\${LOG_FILE}" | head -n 1)
    if [ -z "\${MAC_RAW}" ]; then
        echo "[\${DEVICE_NAME}] Warning: MAC not found in flash log"
        return 1
    fi

    MAC_CLEAN=\$(echo "\${MAC_RAW}" | tr -d ':' | tr '[:upper:]' '[:lower:]')
    touch "\${MAC_RECORD_FILE}"
    if ! grep -Fxq "\${MAC_CLEAN}" "\${MAC_RECORD_FILE}"; then
        echo "\${MAC_CLEAN}" >> "\${MAC_RECORD_FILE}"
        echo "[\${DEVICE_NAME}] MAC recorded: \${MAC_CLEAN}"
    else
        echo "[\${DEVICE_NAME}] MAC already exists: \${MAC_CLEAN}"
    fi
}

flash_single() {
    local PORT=\$1
    local DEVICE_NAME=\$(basename "\$PORT")
    echo "[\${DEVICE_NAME}] Flashing..."
    if [ ! -e "\${PORT}" ]; then
        echo "[\${DEVICE_NAME}] ERROR: Port not found: \${PORT}"
        return 1
    fi
    local LOG_FILE="/tmp/flash_\${DEVICE_NAME}.log"
    local RETRY=0
    local MAX_RETRY=3
    while [ \$RETRY -lt \$MAX_RETRY ]; do
        RETRY=\$((RETRY + 1))
        echo "[\${DEVICE_NAME}] Attempt \${RETRY}/\${MAX_RETRY}..."
        if \${ESPTOOL} --chip \${CHIP} -p \${PORT} -b \${BAUD} \\
            --before=default_reset --after=no_reset --no-stub write_flash \\
${FIRMWARE_LIST}            > "\${LOG_FILE}" 2>&1; then
            echo "[\${DEVICE_NAME}] Flash OK"
            record_mac_from_log "\${LOG_FILE}" "\${DEVICE_NAME}" || true
            return 0
        fi
        if [ \$RETRY -lt \$MAX_RETRY ]; then
            echo "[\${DEVICE_NAME}] Retry in 2 seconds..."
            sleep 2
        fi
    done
    echo "[\${DEVICE_NAME}] Flash FAILED! Log: \${LOG_FILE}"
    return 1
}

if [ \${READ_MAC_ONLY} -eq 1 ]; then
    OP_FUNC=record_mac
else
    OP_FUNC=flash_single
fi

ALL_PORTS=()
for arg in "\$@"; do
    if [[ "\$arg" == *"*"* ]] || [[ "\$arg" == *"?"* ]]; then
        for port in \$arg; do
            [ -e "\$port" ] && ALL_PORTS+=("\$port")
        done
    else
        [ -e "\$arg" ] && ALL_PORTS+=("\$arg") || echo "Warning: Port not found: \$arg"
    fi
done

if [ \${#ALL_PORTS[@]} -eq 0 ]; then
    ALL_PORTS=("/dev/ttyUSB0")
fi

if [ \${#ALL_PORTS[@]} -eq 1 ]; then
    ${OP_FUNC} "\${ALL_PORTS[0]}"
else
    PIDS=()
    FAILED=()
    for PORT in "\${ALL_PORTS[@]}"; do
        ${OP_FUNC} "\$PORT" &
        PIDS+=(\$!)
    done
    for i in "\${!PIDS[@]}"; do
        if ! wait "\${PIDS[\$i]}"; then
            FAILED+=("\${ALL_PORTS[\$i]}")
        fi
    done
    if [ \${#FAILED[@]} -gt 0 ]; then
        echo "Failed: \${FAILED[*]}"
        exit 1
    fi
fi
SCRIPT_EOF
chmod +x "${OUTPUT_DIR}/flash.sh"

cat > "${OUTPUT_DIR}/flash_one.bat" << WIN_ONE_EOF
@echo off
setlocal enabledelayedexpansion
set "BAUD=460800"
set "CHIP=esp32s2"
set "ESPTOOL=tools\esptool\esptool.exe"
set "FLASH_PARAMS=${FLASH_PARAMS}"
set "MAC_RECORD_FILE=mac_addresses.txt"
set "READ_MAC=0"
set "PORT=%~1"
if /I "%~1"=="--read-mac" (
    set "READ_MAC=1"
    set "PORT=%~2"
)
if "%PORT%"=="" set "PORT=COM1"
if not exist "%ESPTOOL%" (
    echo ERROR: esptool.exe not found
    exit /b 1
)
if %READ_MAC%==1 (
    call :record_mac
    exit /b 0
)
set /a RETRY=0
set /a MAX_RETRY=3
:retry
set /a RETRY+=1
echo [%PORT%] Attempt %RETRY%/%MAX_RETRY%...
"%ESPTOOL%" --chip %CHIP% -p %PORT% -b %BAUD% --before=default_reset --after=no_reset --no-stub write_flash %FLASH_PARAMS%${WIN_FIRMWARE_ARGS}
if %errorlevel%==0 (
    echo [%PORT%] Flash OK
    call :record_mac
    exit /b 0
)
if %RETRY% LSS %MAX_RETRY% (
    timeout /t 2 /nobreak >nul
    goto retry
)
exit /b 1

:record_mac
set "MAC_OUT=%TEMP%\mo_%RANDOM%.txt"
set "PSCMD=%ESPTOOL% --chip %CHIP% -p %PORT% -b %BAUD% read_mac"
echo [%PORT%] Reading MAC...
powershell -NoProfile -ExecutionPolicy Bypass -Command "!PSCMD! 2>&1 | Select-String -Pattern '([0-9a-fA-F]{2}:){5}[0-9a-fA-F]{2}' | Select-Object -First 1 | ForEach-Object { \$_.Matches.Value.Replace(':','').ToLower() } | Out-File -FilePath '!MAC_OUT!' -Encoding ASCII -NoNewline"
echo -----------------------------------
if exist "!MAC_OUT!" (
    set /p MAC_CLEAN=<"!MAC_OUT!"
    echo [DEBUG] Raw MAC from file: !MAC_CLEAN!
) else (
    echo [ERROR] File !MAC_OUT! does not exist.
)
del "%MAC_OUT%" 2>nul
if not defined MAC_CLEAN goto :eof
if not exist "%MAC_RECORD_FILE%" type nul > "%MAC_RECORD_FILE%"
findstr /I /X /C:"!MAC_CLEAN!" "%MAC_RECORD_FILE%" >nul 2>nul
if errorlevel 1 echo(!MAC_CLEAN!>>"%MAC_RECORD_FILE%"
goto :eof
WIN_ONE_EOF

cat > "${OUTPUT_DIR}/flash.bat" << WIN_MAIN_EOF
@echo off
setlocal enabledelayedexpansion
set "PORTS="
set "SINGLE_PORT="
set "MODE=flash"

REM Parse arguments
if /I "%~1"=="--read-mac" (
    set "MODE=--read-mac"
    shift
)
if /I "%~1"=="all" (
    for /f "tokens=3" %%a in ('reg query HKLM\HARDWARE\DEVICEMAP\SERIALCOMM 2^>nul ^| findstr /I "COM"') do (
        set "PN=%%a"
        set "PN=!PN:\\0=!"
        set "PN=!PN: =!"
        if not "!PN!"=="" set "PORTS=!PORTS! !PN!"
    )
    if "!PORTS!"=="" exit /b 1
    goto multi
)

if "%~2"=="" (
    set "SINGLE_PORT=%~1"
    goto single
)

REM Collect ports from args
:collect
if "%~1"=="" goto done_collect
set "PORTS=!PORTS! %~1"
shift
goto collect
:done_collect

if not "!PORTS!"=="" goto multi
exit /b 1

:single
if "!SINGLE_PORT!"=="" exit /b 1
if "!MODE!"=="--read-mac" (
    call flash_one.bat --read-mac !SINGLE_PORT!
) else (
    call flash_one.bat !SINGLE_PORT!
)
exit /b %errorlevel%

:multi
for %%P in (!PORTS!) do (
    echo [%%P] Starting flash...
    if "!MODE!"=="--read-mac" (
        start /b "Flash %%P" cmd /c "cd /d %~dp0 && call flash_one.bat --read-mac %%P"
    ) else (
        start /b "Flash %%P" cmd /c "cd /d %~dp0 && call flash_one.bat %%P"
    )
)
goto wait

:wait
timeout /t 1 /nobreak >nul
tasklist /fi "imagename eq esptool.exe" /fo csv 2>nul | findstr /i "esptool.exe" >nul
if %errorlevel%==0 goto wait
exit /b 0
WIN_MAIN_EOF



echo "Windows scripts generated"

echo "Copying firmware files..."
while read -r line; do
    [ -z "$line" ] && continue
    file=$(echo "$line" | awk '{print $2}')
    src_file="${BUILD_DIR}/${file}"
    if [ -f "${src_file}" ]; then
        cp "${src_file}" "${OUTPUT_DIR}/"
        echo "  Copied: $(basename "$file")"
    else
        echo "  Warning: file not found: ${src_file}"
    fi
done < <(tail -n +2 "${FLASH_ARGS_FILE}")

cp "${FLASH_ARGS_FILE}" "${OUTPUT_DIR}/flash_args.txt"

echo "Copying Windows esptool..."
if [ -f "${ESPTOOL_SOURCE}" ]; then
    mkdir -p "${OUTPUT_DIR}/${ESPTOOL_WIN_DIR}"
    cp "${ESPTOOL_SOURCE}" "${OUTPUT_DIR}/${ESPTOOL_WIN_DIR}/"
    echo "  Copied: esptool.exe"
fi

cat > "${OUTPUT_DIR}/README.txt" << 'README_EOF'
ESP32-S2 MINIBOX Production Firmware Pack

Files:
- flash.sh          Linux/Mac flash script
- flash.bat         Windows launcher
- flash_one.bat     Windows single-port flash with retry
- mac_addresses.txt Recorded MAC addresses (deduplicated, lowercase, no ':')
- flash_args.txt    Flash parameters reference
- mac_addresses.txt Recorded MAC addresses, deduplicated and stored without ':'
- *.bin             Firmware files

Usage:
Linux/Mac:
Linux/Mac:
  ./flash.sh
  ./flash.sh --read-mac
  ./flash.sh --read-mac /dev/ttyUSB0
  ./flash.sh /dev/ttyUSB1
  ./flash.sh /dev/ttyUSB0 /dev/ttyUSB1
  ./flash.sh /dev/ttyUSB*

Windows:
  flash.bat
  flash.bat --read-mac
  flash.bat --read-mac COM3
  flash.bat COM3
  flash.bat COM1 COM2 COM3
  flash.bat all
README_EOF

echo "Creating ZIP package..."
cd "${OUTPUT_DIR}"
zip -r "${ZIP_NAME}" . -x "${ZIP_NAME}"
mv "${ZIP_NAME}" "${BUILD_DIR}/"

echo "Pack complete!"
echo "Output dir: ${OUTPUT_DIR}"
echo "Package: ${BUILD_DIR}/${ZIP_NAME}"


path: scripts/usbipd_attach_serial.sh
content: #!/usr/bin/env bash
set -euo pipefail

# Auto map a USB serial device from Windows to WSL2 via usbipd
# Usage:
#   usbipd_attach_serial.sh [--busid <BUSID>] [--distro <DISTRO>] [--keyword <TEXT>] [--dry-run]

BUSID=""
DISTRO=""
KEYWORD=""
DRY_RUN=0

usage() {
  cat <<'EOF'
Usage:
  usbipd_attach_serial.sh [--busid <BUSID>] [--distro <DISTRO>] [--keyword <TEXT>] [--dry-run]

Options:
  --busid <BUSID>    Specify bus id directly, e.g. 2-1
  --distro <DISTRO>  WSL distro name for attach command (optional)
  --keyword <TEXT>   Filter device line by keyword (e.g. ESP32, COM37, CP210x)
  --list             List all matching devices and exit
  --dry-run          Print command only, do not execute
  --bind             Only bind the device (skip attach), useful for first-time setup
  -h, --help         Show help

Behavior:
  1) Reads `usbipd list` from Windows PowerShell
  2) Auto-selects first "Connected" serial-like device (prefer STATE=Shared)
  3) Executes: usbipd bind --busid=<BUSID> (admin required, makes device shareable)
  4) Executes: usbipd attach --wsl --busid=<BUSID> [--distribution <DISTRO>]
  5) Prints detected /dev/ttyACM* and /dev/ttyUSB* in WSL
  (Use --bind to only perform bind, skip attach)

Multiple devices:
  When --keyword matches multiple devices, all matching devices will be bound/attached.
EOF
}

BIND_ONLY=0
LIST_MODE=0
while [[ $# -gt 0 ]]; do
  case "$1" in
    --busid) BUSID="${2:-}"; shift 2;;
    --distro) DISTRO="${2:-}"; shift 2;;
    --keyword) KEYWORD="${2:-}"; shift 2;;
    --list) LIST_MODE=1; shift;;
    --dry-run) DRY_RUN=1; shift;;
    --bind) BIND_ONLY=1; shift;;
    -h|--help) usage; exit 0;;
    *) echo "Unknown arg: $1" >&2; usage; exit 2;;
  esac
done

PS_LIST=$(powershell.exe -NoProfile -Command "usbipd list" | tr -d '\r')

echo "=== usbipd list ==="
echo "$PS_LIST"

get_serial_devices() {
  local lines
  lines=$(echo "$PS_LIST" | awk '
    BEGIN{inConnected=0}
    /^Connected:/ {inConnected=1; next}
    /^Persisted:/ {inConnected=0}
    { if (inConnected) print }
  ')

  # Keep likely serial rows only
  local serial_rows
  serial_rows=$(echo "$lines" | grep -E "USB 串行设备|USB JTAG/serial debug unit|USB-Enhanced-SERIAL|CP210|CH34|FTDI|Serial|UART|ESP32|COM" || true)

  if [[ -n "$KEYWORD" ]]; then
    serial_rows=$(echo "$serial_rows" | grep -i -- "$KEYWORD" || true)
  fi

  echo "$serial_rows"
}

auto_pick_busid() {
  local serial_rows="$1"
  # Prefer Shared first
  local preferred
  preferred=$(echo "$serial_rows" | grep -E "Shared\s*$" | head -n1 || true)
  if [[ -z "$preferred" ]]; then
    preferred=$(echo "$serial_rows" | head -n1 || true)
  fi

  if [[ -n "$preferred" ]]; then
    echo "$preferred" | awk '{print $1}'
  fi
}

# Get all matching serial devices
ALL_DEVICES=$(get_serial_devices)

# List mode: print matching devices and exit
if [[ "$LIST_MODE" == "1" ]]; then
  if [[ -z "$ALL_DEVICES" ]]; then
    echo "No matching serial devices found."
    exit 0
  fi
  echo ""
  echo "=== Matching serial devices ==="
  echo "$ALL_DEVICES"
  exit 0
fi

# Auto-pick busid if not specified
if [[ -z "$BUSID" ]]; then
  BUSID=$(auto_pick_busid "$ALL_DEVICES" || true)
fi

if [[ -z "$BUSID" ]]; then
  echo "ERROR: No candidate BUSID found. Please specify --busid <BUSID> or use --list to see available devices." >&2
  exit 1
fi

# Get all matching BUSIDs for batch processing
ALL_BUSIDS=$(echo "$ALL_DEVICES" | awk '{print $1}')

# When using keyword and multiple devices found, process all of them
if [[ -n "$KEYWORD" && $(echo "$ALL_BUSIDS" | wc -l) -gt 1 ]]; then
  echo ""
  echo "=== Multiple devices matched, processing all ==="
  echo "$ALL_DEVICES"
  echo ""
fi

# Process each BUSID
process_device() {
  local busid="$1"
  
  local cmd="usbipd attach --wsl --busid=$busid"
  if [[ -n "$DISTRO" ]]; then
    cmd+=" --distribution $DISTRO"
  fi

  echo ""
  echo "--- Processing BUSID: $busid ---"
  echo "Attach command: $cmd"

  # Bind the device first (admin may be required for first-time bind)
  local bind_cmd="usbipd bind --busid=$busid"
  echo "Bind command: $bind_cmd"

  if [[ "$DRY_RUN" == "1" ]]; then
    echo "[dry-run] not executing bind/attach"
    return 0
  fi

  # Execute bind (may fail if already bound, that's ok)
  set +e
  local bind_out
  bind_out=$(powershell.exe -NoProfile -Command "$bind_cmd" 2>&1)
  local bind_rc=$?
  set -e
  if [[ $bind_rc -ne 0 ]]; then
    if echo "$bind_out" | grep -qi "already bound\|already shared"; then
      echo "[info] device already bound/shared, continue."
    else
      echo "[warn] bind failed (may require admin): $bind_out"
    fi
  else
    echo "$bind_out"
  fi

  if [[ "$BIND_ONLY" == "1" ]]; then
    echo "[info] bind-only mode, skipping attach."
    return 0
  fi

  set +e
  local attach_out
  attach_out=$(powershell.exe -NoProfile -Command "$cmd" 2>&1)
  local attach_rc=$?
  set -e
  echo "$attach_out"
  if [[ $attach_rc -ne 0 ]]; then
    if echo "$attach_out" | grep -qi "already attached"; then
      echo "[info] device already attached, continue."
    else
      echo "[error] attach failed" >&2
      return $attach_rc
    fi
  fi
}

# Process single device or all devices
if [[ -n "$KEYWORD" && $(echo "$ALL_BUSIDS" | wc -l) -gt 1 ]]; then
  # Multiple devices mode - process each busid
  for busid in $ALL_BUSIDS; do
    [[ -n "$busid" ]] && process_device "$busid"
  done
else
  # Single device mode
  process_device "$BUSID"
fi

echo ""
echo "=== WSL serial nodes ==="
found=0
# Check various serial device patterns
for pat in /dev/ttyACM* /dev/ttyUSB*; do
  for dev in $pat; do
    if [[ -e "$dev" ]]; then
      echo "$dev"
      found=1
    fi
  done
done
if [[ "$found" -eq 0 ]]; then
  echo "No /dev/ttyACM* or /dev/ttyUSB* yet."
fi

# Also show USB device info if available
if command -v lsusb >/dev/null 2>&1; then
  echo ""
  echo "=== USB devices (lsusb) ==="
  lsusb | grep -i "serial\|uart\|ch34\|cp210\|ftdi" || echo "No matching USB serial devices found in lsusb"
fi

echo ""
echo "Done."


Security scan verdicts — ClawScan: suspicious | static analysis: clean | VirusTotal: suspicious | SkillSpector: suspicious (MEDIUM)