tree: a81ef8df67aa49ab332f575066ac4db3c4485448 [search]
  1. .agents/
  2. cpp/
  3. end_to_end_tests/
  4. fidl/
  5. python/
  6. scripts/
  7. src/
  8. tests/
  9. .gitignore
  10. base-tooling-requirements.txt
  11. BUILD.gn
  12. OWNERS
  13. pyproject.toml
  14. README.md
  15. requirements.txt
src/developer/fuchsia-controller/README.md

Fuchsia Controller

fuchsia-controller (fuchsia_controller_py) is a host-side Python library for interacting with Fuchsia target devices over FDomain/RCS and invoking FIDL protocols directly from Python.

Building Python Wheels for Interactive & Virtualenv Usage

//src/developer/fuchsia-controller/BUILD.gn defines PEP 427 Python wheel targets that package fuchsia-controller and the Fuchsia FIDL SDK (partner_idk, prebuilt, and host_tool FIDL libraries) so they can be installed into any virtual environment pinned to fuchsia-vendored-python:

  • //src/developer/fuchsia-controller:fuchsia_controller_combined_wheel (alias :fuchsia_controller_with_fidl_wheel): Builds a single self-contained fuchsia_controller-*.whl containing fuchsia_controller_py, fuchsia_controller_internal, fidl, and all ~270 SDK fidl_fuchsia_* Python modules.
  • //src/developer/fuchsia-controller:fuchsia_controller_wheel and //src/developer/fuchsia-controller:fidl_sdk_wheel (alias :fidl_wheel): Separate wheels if you prefer to install fuchsia-controller and fidl-sdk independently.

Build the combined wheel once:

fx build --host //src/developer/fuchsia-controller:fuchsia_controller_combined_wheel

The generated wheel is placed at:

$(fx get-build-dir)/host_x64/obj/src/developer/fuchsia-controller/fuchsia_controller_combined_wheel/fuchsia_controller-0.1.0-*.whl

Setting Up a Virtual Environment (uv or venv)

Because fuchsia_controller_internal is a native C++ extension compiled against Fuchsia‘s prebuilt Python interpreter (fuchsia-vendored-python), the virtual environment must use Fuchsia’s vendored Python binary rather than the system /usr/bin/python3.

Option 1: Using uv (Recommended)

FUCHSIA_PYTHON="$(fx get-workspace-dir)/prebuilt/third_party/python3/linux-x64/bin/python3"
WHEEL_DIR="$(fx get-build-dir)/host_x64/obj/src/developer/fuchsia-controller/fuchsia_controller_combined_wheel"

# Create a persistent virtual environment and install the combined wheel:
uv venv /tmp/fc_venv --python "$FUCHSIA_PYTHON"
uv pip install --python /tmp/fc_venv/bin/python \
  --no-index --find-links "$WHEEL_DIR" fuchsia-controller

# Launch an interactive Python REPL (python -m asyncio for top-level await):
/tmp/fc_venv/bin/python -m asyncio

# Or run a standalone script ephemerally in a single command without creating a named venv:
uv run --no-project --python "$FUCHSIA_PYTHON" \
  --no-index --find-links "$WHEEL_DIR" \
  --with fuchsia-controller my_script.py

Option 2: Using fuchsia-vendored-python -m venv

WHEEL_DIR="$(fx get-build-dir)/host_x64/obj/src/developer/fuchsia-controller/fuchsia_controller_combined_wheel"

fuchsia-vendored-python -m venv /tmp/fc_venv
/tmp/fc_venv/bin/pip install --no-index --find-links "$WHEEL_DIR" fuchsia-controller

# Launch an interactive async REPL or run a script:
/tmp/fc_venv/bin/python -m asyncio
/tmp/fc_venv/bin/python my_script.py

Interactive & Standalone Script Example

By default, Context() creates a temporary isolated IsolateDir(), which does not load ~/.local/share/Fuchsia/ffx/config/.ffx_user_config.json and creates a local testing FDomain if target=None. When connecting to a live target (such as an emulator, SSH target, or USB/Cadmium device), pass an explicit target nodename/address and load .ffx_user_config.json:

import asyncio
import json
import pathlib
import subprocess
from typing import Any

from fuchsia_controller_py import Context
import fidl_fuchsia_buildinfo as f_buildinfo
import fidl_fuchsia_developer_remotecontrol as f_remotecontrol


def create_device_context(target: str | None = None) -> Context:
    """Creates a Context preserving user-level ffx connectivity settings."""
    user_cfg_path = (
        pathlib.Path.home()
        / ".local/share/Fuchsia/ffx/config/.ffx_user_config.json"
    )
    config: dict[str, Any] = (
        json.loads(user_cfg_path.read_text()) if user_cfg_path.exists() else {}
    )
    config.pop("target.default", None)
    if isinstance(config.get("target"), dict):
        config["target"].pop("default", None)
        if not config["target"]:
            config.pop("target", None)
    else:
        config.pop("target", None)

    if not target:
        out = subprocess.check_output(
            ["ffx", "target", "list", "--format", "json"], text=True
        )
        targets = json.loads(out)
        defaults = [t for t in targets if t.get("is_default")]
        chosen = defaults[0] if defaults else (targets[0] if targets else None)
        if not chosen:
            raise RuntimeError("No Fuchsia target found via `ffx target list`")
        target = chosen["nodename"]

    return Context(target=target, config=config)


async def main() -> None:
    ctx = create_device_context()

    # Query RemoteControl (RCS)
    rcs = f_remotecontrol.RemoteControlClient(ctx.connect_remote_control_proxy())
    ident = (await rcs.identify_host()).unwrap()
    print(f"Connected to nodename: {ident.response.nodename}")

    # Connect to any component capability by moniker and Marker
    build_info = f_buildinfo.ProviderClient(
        ctx.connect_device_proxy("core/build-info", f_buildinfo.ProviderMarker)
    )
    info = await build_info.get_build_info()
    print(f"Product: {info.build_info.product_config}, Board: {info.build_info.board_config}")


if __name__ == "__main__":
    asyncio.run(main())