Repository CLI

September 19, 2026 ยท View on GitHub

Use this reference when you already know which ESPectre workflow you need and want the repository command for it. Start with SETUP.md if you have not chosen a frontend yet. Frontend READMEs own configuration, prerequisites, and device-specific troubleshooting.

The command tables below are summaries; ./espectre --help and ./espectre <namespace> --help are authoritative for current flags.

Launchers

HostLauncher
macOS/Linux./espectre
Windows PowerShell/CMD.\espectre.cmd

Run the CLI from the repository root.

Local build prerequisites

The maintained host workflows target Python 3.14. Create the repository environment with that interpreter before a local firmware build or host-tool workflow:

python3 -m venv .venv
source .venv/bin/activate
python -m pip install -r requirements.txt

On Windows PowerShell, create the environment with py -3 -m venv .venv, activate .\.venv\Scripts\Activate.ps1, and run the same install command.

Native, Matter, and Micro-ESPectre firmware builds use one shared backend policy: prefer an active IDF_PATH environment, a standard local ESP-IDF installation, or the pinned ESP-IDF toolchain already managed by ESPHome, and automatically fall back to the pinned ESP-IDF Docker image when none is available. Repository ESPHome commands explicitly select its native esp-idf toolchain and never use PlatformIO.

./espectre native build --chip c3

On Windows, use .\espectre.cmd native build --chip c3. The same pattern applies to Matter.

When the local environment is absent and Docker is running, a cached image is used without prompting. If the image is missing, an interactive build asks before downloading it; non-interactive builds must opt in with --pull missing. If Docker is installed but stopped, the CLI asks you to start it and retry. Use --backend local or --backend docker to require one path, and use ./espectre doctor to inspect only the local ESP-IDF environment.

Docker covers firmware compilation only; flashing still uses host serial tooling. If neither build backend is available, build an ESPHome configuration once to provision its native toolchain, install Docker, or install ESP-IDF 5.5.5 with the official ESP-IDF Get Started flow.

Building against an SDK bundle

Repository builds compile the SDK from the current checkout by default. To build Native, Matter, ESPHome, or Micro-ESPectre against an extracted GitHub/web SDK bundle, set ESPECTRE_SDK_ROOT to the absolute path of its src/cpp directory. Run the command for your frontend from the repository root:

ESPECTRE_SDK_ROOT=/path/to/extracted-sdk/src/cpp ./espectre native build --chip c3
ESPECTRE_SDK_ROOT=/path/to/extracted-sdk/src/cpp ./espectre matter build --chip c3
ESPECTRE_SDK_ROOT=/path/to/extracted-sdk/src/cpp ./espectre esphome build --chip c3
ESPECTRE_SDK_ROOT=/path/to/extracted-sdk/src/cpp ./espectre micro build --chip c3

Native, Matter, and Micro-ESPectre pass the selected SDK path to CMake. Changing or unsetting ESPECTRE_SDK_ROOT reconfigures an existing build. Their Docker backend forwards the selection and mounts bundles outside the checkout read-only at distinct container paths. Add --backend docker to select that backend. Micro-ESPectre resolves its firmware version from the frontend checkout independently of the selected SDK.

These builds check that the firmware can consume the packaged public SDK without relying on SDK files from the repository checkout. ARCHITECTURE.md describes the source boundaries; each frontend README covers its integration details.

Optional compiler cache

ccache is optional. It shortens repeat ESP-IDF builds, especially Matter builds, by reusing unchanged compiler output across build directories. The local ./espectre backend enables it automatically when ccache is on PATH. Repository Docker builds enable a persistent cache automatically, so the Docker backend needs no host installation.

Install ccache for the local backend:

  • macOS with Homebrew: brew install ccache
  • Debian or Ubuntu Linux: sudo apt update && sudo apt install ccache; on other distributions, install the ccache package with the system package manager
  • Windows: the official ESP-IDF Tools installation includes ccache; verify it from an ESP-IDF PowerShell with ccache --version. For a manually managed toolchain, install the official Windows release or run choco install ccache when Chocolatey is available

Confirm the binary is on PATH:

ccache --version

./espectre native build --chip c3, ./espectre matter build --chip c3, and ./espectre doctor then print Compiler cache: ccache when the cache is active. Replace c3 with the selected chip. Set IDF_CCACHE_ENABLE=0 to disable it for one shell. An explicit IDF_CCACHE_ENABLE=1 remains supported for toolchains that do not go through the repository wrapper.

Use the equivalent PowerShell environment variable on Windows to disable the cache:

$env:IDF_CCACHE_ENABLE = "0"
ccache --version

The frontend workflow sections below cover build cleanup, chip-matched flash selection, and namespace-specific flags.

Generated files and caches

Repository workflows keep generated files under .cache/: firmware/ and sdk/ contain distribution files, reports/ contains audit and coverage reports, build/ contains Docker toolchain homes, and npz/, ruff/, and pytest/ contain reusable caches. The virtual environment remains in .venv/, and ESP-IDF and ESPHome retain their frontend-specific build directories.

Python bytecode follows the interpreter defaults or the user's PYTHONPYCACHEPREFIX setting; repository scripts do not set a bytecode cache location. NPZ tooling continues to support ESPECTRE_NPZ_CACHE_DIR for a cache on another volume. Moving an existing .npz_cache/ to .cache/npz/ preserves its contents; avoid moving caches while a build, test, or training process is using them.

Command Map

NamespacePurpose
esphomeBuild, flash, validate, or monitor the ESPHome frontend
nativeBuild or flash the native ESP-IDF frontend
matterBuild, flash, or read onboarding data from the Matter ESP-IDF frontend
microBuild, flash, deploy, run, and verify the research frontend
monitorAttach to serial logs with auto-reconnect support
devicesDiscover advertised ESPectre devices on the local network
provisionProvision Native or ESPHome Wi-Fi through Improv Serial
directSend one Direct HTTP protocol request to a device
collectRun live CSI inspection and dataset collection flows
doctorValidate the local ESP-IDF environment used by the wrapper
mqttOpen the interactive MQTT shell
versionShow the CLI version label
aboutShow project and CLI information

Common Patterns

  • Use ./espectre --help for the current top-level command list.
  • Use ./espectre <namespace> --help for namespace-specific flags.
  • The wrapper prefers repository defaults and shared host autodetection over long manual setup steps.
  • Native and Matter prefer the local ESP-IDF environment detected by the wrapper, including the native toolchain managed by the pinned ESPHome installation, and fall back to Docker for builds when no local installation is available. Use ./espectre doctor to inspect the local ESP-IDF path.
  • Serial selection is shared across published frontend flash, monitor, provision, and onboarding operations. It classifies the physical console (uart, usb_cdc, or usb_serial_jtag) from non-destructive USB metadata and never opens or resets candidates. Native and ESPHome provision and monitor operations, and Matter monitor operations, prefer a single native USB console over a secondary UART bridge. If several equally suitable ports remain, the resolver prompts for a selection. Pass --port to require that exact compatible device. Serial flash commands require --chip, and esptool verifies the connected target while flashing. UART and USB Serial/JTAG use esptool's normal loader entry; ESP32-S2 USB CDC must already be in download mode.

Frontend Workflow Commands

esphome

The esphome namespace exposes:

CommandPurpose
buildBuild the selected ESPHome firmware
flashFlash the selected ESPHome firmware
configValidate and render the selected config
monitorOpen logs for the selected config

Common flags include --chip, --config, and --device. Serial flash and monitor follow the shared --chip selection rule when --device is omitted or names a serial port. esphome flash --firmware <path> uploads a prebuilt image instead of the most recent local build: serial flashing expects an ESPHome factory image written at offset 0x0, while a hostname or IP address expects an ESPHome OTA image.

./espectre esphome flash --chip c6 --device espectre-<mac-suffix>.local --firmware espectre-esphome-3.0.0-esp32c6-ota.bin

esphome flash --erase clears all flash data before a serial upload. It resolves or requires a serial device and cannot be combined with an OTA hostname.

Each chip uses one canonical example. The repository CLI keeps that device configuration and switches the ESPectre component source from GitHub to the local checkout.

esphome build --json emits one final JSON object after the normal build log. It identifies the frontend, chip, exact application artifact, byte size, and SHA-256 digest for machine consumers. Omitting --config keeps canonical config selection inside the CLI.

The wrapper explicitly selects ESPHome's native esp-idf toolchain for every command. It does not use the legacy PlatformIO build backend.

For build, cleanup flags are:

  • --clean: run esphome clean for the selected config before compiling.
  • --clean-all: run esphome clean-all for the config root before compiling.

native and matter

The Native and Matter namespaces expose build and flash:

CommandPurpose
buildConfigure the chip target and build the firmware
flashFlash the last successfully published firmware using host esptool

For build, cleanup flags are:

  • --clean: remove only the resolved build directory for the selected chip, such as build-esp32c3.
  • --clean-all: remove all frontend build directories plus shared artifacts such as sdkconfig, sdkconfig.old, and dependencies.lock.

Build environment flags are:

  • --backend auto: prefer local ESP-IDF and use Docker only when no local installation is detected; this is the default.
  • --backend local: require local ESP-IDF and do not consider Docker.
  • --backend docker: require the pinned ESP-IDF Docker image.
  • --pull ask|missing|never: ask before downloading a missing Docker image, download it automatically, or require it to be cached. The default is ask; non-interactive jobs should use missing or never explicitly.

native build --json and matter build --json emit the same final build-metadata object as ESPHome, including the exact artifact selected from the resolved chip build directory.

Each chip build directory owns its generated sdkconfig. The CLI passes IDF_TARGET directly to CMake, so an ordinary build never runs set-target or its implicit fullclean and cannot invalidate another chip's configuration.

Local builds enable ccache automatically when the binary is on PATH. Docker builds already keep a persistent compiler cache. Set IDF_CCACHE_ENABLE=0 to disable the local cache.

Docker builds use a separate directory such as build-esp32c3-docker, which prevents host and container CMake caches from sharing incompatible absolute paths. Docker is a build backend only; flash uses the host esptool installation and serial port.

After a successful local or Docker build, the CLI copies all flash binaries into a shared directory for that frontend and chip, such as build-flash-esp32c5. Files are named by their content hash, and flasher_args.json is replaced atomically only after all files are ready. A failed build or publication leaves the previous image selected. Older binaries remain available to a flash already in progress; --clean-all explicitly removes published images as well as compilation caches.

For flash, the required --chip selects the last successfully published firmware in that shared directory, regardless of its build backend. The CLI reads the published flasher_args.json metadata and passes its flash settings and files to one esptool operation using esptool 5 option names. --erase adds write-flash --erase-all. The verified image starts through --after watchdog-reset, except on classic ESP32, where esptool requires --after hard-reset. On Matter, erasing also removes the persisted onboarding identity, so the next boot generates new onboarding codes.

flash never configures or rebuilds firmware, and it does not require Docker or ESP-IDF to be available. If no published image exists, it directs the operator to run the matching build command first. ESPECTRE_IDF_BUILD_DIR remains an explicit override for the compilation directory and the directory read by flash.

Matter also exposes:

CommandPurpose
qrReset the connected device and print its persisted QR payload and manual pairing code

Use matter qr --json or matter flash --json when another tool must consume onboarding data. The final JSON object contains the selected port, chip, QR payload, and manual code; treat that output as a commissioning secret.

qr requires --chip, uses the shared serial selection, and asks esptool to start the installed application before reading its onboarding output unless --no-reset is supplied.

Examples:

./espectre native build --chip c3
./espectre native build --chip c3 --backend docker
./espectre native build --chip c3 --clean
./espectre native build --chip c3 --clean-all
./espectre native flash --chip c5
./espectre esphome build --chip c3 --clean
./espectre esphome build --chip c3 --clean-all
./espectre matter build --chip c6
./espectre matter flash --chip c6 --port /dev/cu.usbmodemXXXX
./espectre matter qr --chip c6 --port /dev/cu.usbmodemXXXX

Device And Host Commands

monitor

monitor attaches to a serial port and streams logs.

Common flags:

  • --port
  • --chip
  • --frontend
  • --baud
  • --raw
  • --reset

When --chip is supplied, serial selection follows the shared rule above and keeps ports whose USB console matches the chip. An explicit incompatible --port is rejected. Without --chip, Native and ESPHome monitor and provision, and Matter monitor, automatically prefer a single native USB console over a secondary UART bridge; they prompt without resetting either interface when several equally suitable ports remain. By default, monitor attaches without resetting the device. With --reset, the CLI requires --chip and delegates application start to esptool before opening the monitor. USB CDC consoles such as the ESP32-S2 TinyUSB console still require a manual reset and monitor without --reset.

Example:

./espectre monitor --chip c3 --frontend native --port /dev/cu.usbmodemXXXX

Reset on open:

./espectre monitor --chip c3 --frontend native --port /dev/cu.usbmodemXXXX --reset

devices

devices performs a fresh host-side browse for _espectre._tcp.local. and lists compatible firmware through one first-party record contract. It does not inspect _esphomelib, _matterc, or other upstream service types. The normalized result includes the frontend, device identity, display name, chip, IP address, and Direct HTTP endpoint. DISCOVERY.md defines the record-level contract.

FlagPurpose
--frontend native|esphome|matter|microLimit discovery to one frontend; omit it to browse every supported service
--chip esp32|c3|s2|s3|c5|c6Limit normalized records to one chip family
--timeout <seconds>Set the maximum one-shot browse duration; the default is 2.5 seconds
--jsonEmit machine-readable normalized records for scripts and tooling

Examples:

./espectre devices
./espectre devices --frontend native
./espectre devices --frontend matter --timeout 5
./espectre devices --frontend matter --chip s3 --json
./espectre devices --frontend esphome
./espectre devices --frontend matter
./espectre devices --json

The command uses the repository zeroconf dependency and requires the host and device to share an mDNS-visible network. Each invocation starts a new PTR browse; there is no discovery cache. The timeout is an upper bound rather than an unconditional delay: after the first complete record, discovery returns when no record has been added, changed, or removed for 350 ms. If no device responds, it waits for the full timeout. VLAN boundaries, client isolation, and multicast filtering may hide otherwise reachable devices; explicit IP addresses, Native .local names, remembered endpoints, and Improv Serial remain the deterministic fallbacks.

provision

provision uses the shared Improv Serial v1 client to configure a clean Native or ESPHome device over USB. It accepts the same optional --chip, --frontend, and --port capability-aware selection used by monitor. The command resolves and validates the serial port before reading the Wi-Fi password from ESPECTRE_WIFI_PASSWORD, or from the variable named by --password-env; when the variable is unset, the CLI then prompts without echoing the password. The command validates framing, checksums, state transitions, correlated RPC results, UTF-8 strings, and the returned device URL. Add --json to return the selected port, endpoint, and provisioning evidence to another tool.

ESPECTRE_WIFI_PASSWORD='secret' ./espectre provision --chip c3 --frontend native --port /dev/cu.usbmodemXXXX --ssid MyNetwork

The password is never accepted as a command-line value, printed, or included in the returned endpoint. --timeout bounds the complete state, device-info, and Wi-Fi provisioning exchange.

direct

direct sends one ESPectre resource request. Supply an HTTP verb and relative resource, then use --endpoint with an HTTP(S) device URL or --frontend to discover a device. Add --chip to narrow frontend discovery before selection. When discovery returns multiple matching records, the CLI prompts for an explicit selection.

./espectre direct get health --frontend native
./espectre direct get diagnostics --endpoint http://espectre-0123456789abcdef.local
./espectre direct get diagnostics --data '{"fields":["traffic_tx_pps","csi_hw_error_total"]}' --endpoint http://espectre-0123456789abcdef.local
./espectre direct get diagnostics --data '{"fields":[]}' --endpoint http://espectre-0123456789abcdef.local
./espectre direct patch sensing --frontend esphome --data '{"detector":"high_accuracy"}'
./espectre direct post sensing/calibrations --frontend matter --chip s3

The client sends the exact allowed https://test.espectre.dev Origin by default, limits mutation JSON to 2,048 bytes, accepts a response up to 8,192 bytes, validates direct resource snapshots or mutation results, and closes cleanly. It negotiates protocol 1.0 once through capabilities; messages do not repeat the version. Use --origin only for another exact Origin already allowed by the firmware; the CLI does not weaken device Origin policy.

Access-point selection

./espectre direct post wifi/scans  # start an access-point scan
./espectre direct get wifi/access-points  # list BSSID, channel, and RSSI
./espectre direct put wifi/bssid --data '{"bssid":"AA:BB:CC:DD:EE:FF"}'  # pin one AP

The scan is asynchronous, so wait a few seconds after POST /wifi/scans before reading GET /wifi/access-points. Use --frontend native, --frontend esphome, or --frontend matter to filter discovery, or use --endpoint when you already know the Direct base URL. The station reconnects after a pin or clear.

To restore automatic access-point selection without removing the SSID or password, choose automatic selection in Device settings or run:

./espectre direct delete wifi/bssid

Clear a stale pin after replacing or removing an access point. API.md defines the methods and responses; the frontend README describes persistence.

collect

collect is the HTTP-only host-side CSI collection entry point. One runtime path supports three modes:

  • live inspection when --label is omitted
  • live recording when --label is set
  • read-only dataset inventory when --info is used

Common flags:

FlagPurpose
--targetDevice IP, hostname, full Direct endpoint, or device ID; omit it to discover a raw-capable device
--frontendOptional native, esphome, or matter discovery filter
--source-ipOptional local IPv4 source for hosts with multiple interfaces
--durationStop after N seconds
--labelDataset label for saved collections; use 1-64 ASCII letters, digits, underscores, or hyphens, starting with a letter or digit; omit for live inspection without saving
--start-delayWait N seconds before starting collection; requires --duration
--ppsIntentional external UDP generator rate and nominal dataset rate
--detectorDetector used by the ready gate: lightweight or high_accuracy; a comma-separated list is available only for live comparison
--ready-stable-secondsSeconds below threshold before saved collection starts; set 0 to disable the ready gate

When --target is omitted, collect performs one fresh browse for _espectre._tcp.local. at startup and keeps raw-capable ESPectre records at their advertised Direct port:

  • 0 devices: fail explicitly and suggest --target
  • 1 device: auto-select it
  • N devices: prompt for an interactive choice

The collector uses the same event-driven completion as devices: once a complete record arrives, 350 ms without a changed record completes discovery. If no record arrives, the 2.5-second default timeout is consumed in full.

--target remains the deterministic bypass. The collector resolves an IP, hostname, full Direct endpoint, or full device ID through the same Direct resolver. Native, Matter, and ESPHome use port 62587; a full manually entered endpoint may specify another explicit port, but the resolver does not probe legacy ports.

--info is also read-only: it uses dataset_info.json as the source of truth and prints one table per environment, with label rows and one column per chip.

Live collection negotiates CSI support, persistently sets csi_traffic_mode to external, verifies the resulting resource, starts ExternalTrafficGenerator from tools/ha_traffic_generator_addon/espectre_traffic_generator.py, and only then opens GET /csi. The generator sends the exact four-byte UTF-8 UDP marker "๐Ÿ‘ป".encode("utf-8") (F0 9F 91 BB) at --pps; the device forwards every classified CSI frame without HTTP pacing or temporal decimation. Closing the response ends collection, then the generator stops. The collector intentionally does not restore the previous traffic mode.

Example:

./espectre collect --target 192.168.1.51 --pps 100

Saved files and catalog entries record transport=http, the Direct endpoint, requested and observed PPS, raw protocol version, CSI record version, frontend, chip, firmware, and device ID.

Collection terms:

  • Delivered rate: CSI records received by the collector, measured in packets per second (pps).
  • Admitted rate: records that occupy a detector slot after temporal admission.
  • Excess: extra same-slot records that do not improve occupancy.
  • Backpressure: firmware reports that it cannot transmit records as quickly as they are produced.
  • Queue drop: a classified record rejected because the fixed 16-record raw ring is full.

The external generator is the sole rate owner. HTTP applies no credit window, adaptive rate, sample replacement, or device-side timer. After drain, the invariant fresh_record_total + raw_drop_total == classified_frames_offered_to_raw exposes any hidden loss before the network send.

--detector selects the production detector used for collection readiness. The derived live sensing view applies the production temporal sampler to raw records using the nominal --pps; the saved raw stream remains un-decimated. lightweight performs its normal startup calibration before it can become ready. high_accuracy does not use startup calibration, but still needs its feature window to fill. Live inspection can compare lightweight,high_accuracy in parallel.

When --label is set, saved collection waits for the detector to stay below threshold for --ready-stable-seconds before packets are recorded. Set --ready-stable-seconds 0 to bypass that gate explicitly.

After saving each capture, the collector runs the validator's canonical per-file integrity, signal-quality, temporal-occupancy, and stream-continuity checks. Temporal occupancy is measured on complete production detector windows, warns below 85%, and fails below the shared 70% admission floor. The post-collect summary does not use average packet rate as a quality proxy because excess same-slot records do not improve detector occupancy. A failed capture remains saved for diagnosis, but collect exits unsuccessfully.

When --start-delay is set, --duration is required. The collector waits first, then starts the ordinary generator and capture flow.

The duration is measured from the first received packet for live inspection, or from the start of recording when saving a dataset. Once started, the deadline is checked even when no further packets arrive, with up to one second of polling delay.

For discovery-selected targets, the collector also validates that CSI records carry the same device_id announced over mDNS. If an address was reused by another device, collection aborts instead of saving mixed data under the wrong identity.

Examples:

./espectre devices --frontend native
./espectre collect --target 192.168.1.50 --pps 120
./espectre collect --frontend esphome --pps 120
./espectre collect --label wave --duration 45 --target espectre-0123456789abcdef.local
./espectre collect --label wave --duration 45 --start-delay 15 --target http://192.168.1.50
./espectre collect --info

mqtt

mqtt opens the interactive MQTT shell for ESPectre Protocol devices.

When --device-id is provided, the shell targets that device directly.

When --device-id is not provided, the shell briefly subscribes to:

espectre/v1/devices/+/device
espectre/v1/devices/+/health

It then:

  1. collects device identities from retained device and health plus any live publishes during the scan
  2. shows an interactive selection list
  3. falls back to manual device-id entry if nothing is discovered

After selection, the shell publishes commands to commands/request and subscribes to the matching response and payload topics:

espectre/v1/devices/{device_id}/commands/request
espectre/v1/devices/{device_id}/commands/result
espectre/v1/devices/{device_id}/capabilities
espectre/v1/devices/{device_id}/device
espectre/v1/devices/{device_id}/health
espectre/v1/devices/{device_id}/sensing
espectre/v1/devices/{device_id}/wifi
espectre/v1/devices/{device_id}/ota

After selection, the shell consumes retained capabilities to populate help and tab completion. Mutations and actions return through commands/result; read_diagnostics returns its selected snapshot in data. The CLI requests all values by default; use read_diagnostics fields=traffic_tx_pps,csi_hw_error_total for a subset or read_diagnostics fields=[] for the catalog. Command results annotate the typed prompt line with โœ“ or โœ— code: reason when the terminal allows it. Otherwise, they appear on the next line. Retained state topics are still dumped as YAML.

This behavior belongs to the MQTT transport and applies to ESPectre devices that advertise the MQTT topic surface.

Common MQTT flags:

FlagDefault
--brokerhomeassistant.local or MQTT_BROKER
--port-mqtt1883 or MQTT_PORT
--topic-prefixespectre/v1/devices or MQTT_TOPIC_PREFIX
--device-idExplicit device identifier; otherwise runtime discovery
--usernamemqtt or MQTT_USERNAME
--passwordmqtt or MQTT_PASSWORD

Examples:

./espectre mqtt
./espectre mqtt --device-id 3cf79180d3a0aca4
./espectre mqtt --broker 192.168.1.20 --device-id native-lab

MQTT commands are forwarded to the selected device. The shell keeps only local utilities (help, about, clear, and exit). Help, tab completion, and argument discovery use the device capabilities resource. Unknown or unsupported commands are rejected by the device with a stable result code. Sensing changes use update_sensing, with named values such as threshold=0.35, motion_on_hits=4, and motion_off_hits=3.

check_ota and start_ota accept an optional channel (release, preview, or develop), for example check_ota channel=preview or start_ota channel=develop. Omitting the channel keeps the firmware's build-time default. OTA payloads containing server, manifest, image, or version overrides are rejected by the device. Frontends omit unsupported OTA commands from capabilities.

Native builds accept --ota-channel release|preview|develop. The selected value is compiled into the firmware and is used whenever an MQTT OTA command omits channel; it is propagated through both local and Docker build backends. The default is release, or NATIVE_OTA_CHANNEL when that environment variable is set.

Browser tools such as Flash, Device settings, Monitor, and Theremin live on espectre.dev. Serial logs remain ./espectre monitor.

Utility Commands

CommandPurpose
./espectre doctorValidate the ESP-IDF environment used by the wrapper
./espectre versionShow the current CLI version label
./espectre aboutShow project and CLI information
  • SETUP.md for shared setup, frontend selection, and entry points
  • frontend READMEs under src/cpp/frontend/ for frontend-specific build, flash, provisioning, and protocol details