This guide contains a tutorial for writing a script to remotely interact with a Fuchsia device, various other example scripted interactions, and a primer on implementing a FIDL server in Python on a host.
The methods on this page were originally developed for end-to-end testing, but you may also find them useful for scripting simple device interactions without having to write an ffx
plugin in Rust.
The following technologies enable scripting remote interactions with a Fuchsia device:
Together, Fuchsia Controller and Overnet provide a transport between the host running a Python script and the target Fuchsia device. And the Python FIDL bindings, like FIDL bindings in other languages, facilitate sending and receiving messages from components on the target device.
You generally only need to interact with Python FIDL bindings after connecting to your target device. Connecting to the target device, as you will see, is done with the Context
class from the fuchsia_controller_py
module.
Setting Overnet aside, the libfuchsia_controller_internal.so
shared library (which includes the ABI header) is the core library that drives scripted remote interactions.
The Context
object is the main entry point for all Fuchsia controller interactions. It is responsible for creating and managing connections to Fuchsia devices and is the primary method for getting access to FIDL handles.
When constructed, the Context
object can take a dictionary of configuration key/value pairs, similar to the usage of ffx config
.
config = { "log.dir": "/path/to/logs", "log.level": "trace" } ctx = Context(target="nodename", config=config)
In most cases, a user must specify a target, which is the nodename of a Fuchsia device.
The most common way of using this object is to connect to a specific FIDL protocol on a component running on a Fuchsia device. This is done by calling the connect_to_protocol
method, which takes a component moniker and a protocol name.
This tutorial walks through writing a Python script that reads a list of addresses and prints information about the Fuchsia device at each address.
Note: Most FIDL interfaces should just work. However, some advanced APIs are not supported, e.g., reading from or writing to a VMO.
Please file an issue if you encounter bugs, or have questions or suggestions.
This tutorial requires the following:
Fuchsia source checkout and associated development environment.
Fuchsia device (physical or emulated) reachable via ffx
that exposes the remote control service (RCS).
When running ffx target list
, the field under RCS
must read Y
:
NAME SERIAL TYPE STATE ADDRS/IP RCS fuchsia-emulator <unknown> Unknown Product [fe80::5054:ff:fe63:5e7a%4] Y
(For more information, see Interacting with target devices.)
Update a BUILD.gn
file to include a build target like the following:
import("//build/python/python_binary.gni") assert(is_host) {% includecode gerrit_repo="fuchsia/fuchsia" gerrit_path="tools/fidl/fidlgen_python/BUILD.gn" region_tag="describe_host_example_build_target" %}
The ffx
libraries enable connecting to our Fuchsia device. And the FIDL dependencies make the necessary Python FIDL bindings available to the script.
First, our script needs to import the necessary modules:
{% includecode gerrit_repo="fuchsia/fuchsia" gerrit_path="tools/fidl/fidlgen_python/examples/describe_host.py" region_tag="required_import_block" %}
The fidl_fuchsia_developer_remotecontrol
and fidl_fuchsia_buildinfo
Python modules contains the Python FIDL bindings for the fuchsia.developer.ffx
and fuchsia.buildinfo
FIDL libraries.
The Context
object provides connections to Fuchsia targets.
Next, our script needs to define a function (called describe_host
in this example) to retrieve information from a target device:
{% includecode gerrit_repo="fuchsia/fuchsia" gerrit_path="tools/fidl/fidlgen_python/examples/describe_host.py" region_tag="describe_host_function" %}
This function instantiates a Context
to connect to a Fuchsia device at particular IP address, and then call the fuchsia.developer.remotecontrol/RemoteControl.IdentifyHost
and fuchsia.buildinfo/Provider.GetBuildInfo
methods to get information about the target.
Note: You might notice the component moniker core/build-info
in the script. See the [Finding component monikers][#finding-component-monikers] for how to discover the component moniker for the Fuchsia component you wish to communicate with.
Note: The .unwrap()
called on the result of the IdentifyHost
call is a helper method defined for FIDL result types. It either returns the response contained in the result, if there is one, or raises an AssertionError if the result contains a framework or domain error. (The .response
in this example is a little misleading because the returned RemoteControlIdentifyHostResult
has a response field with type RemoteControlIdentifyHostResponse
that also contains a response field. There are two nested response fields in the value returned by IdentifyHost
).
Finally, you wrap this code with some Python boilerplate to read addresses and print information for each target device. Thus, you arrive at the following script:
{% includecode gerrit_repo="fuchsia/fuchsia" gerrit_path="tools/fidl/fidlgen_python/examples/describe_host.py" region_tag="full_code" %}
This example code lives in //tools/fidl/fidlgen_python
, so you build it with the following command on an x64
host (after adding //tools/fidl/fidlgen_python:examples
to host_labels
with fx args
):
fx build --host //tools/fidl/fidlgen_python:describe_host_example
Next, you use ffx target list
to identify the address of our target device.
$ ffx target list NAME SERIAL TYPE STATE ADDRS/IP RCS fuchsia-emulator <unknown> core.x64 Product [127.0.0.1:34953] Y
Then you run the script!
$ fx run-in-build-dir host_x64/obj/tools/fidl/fidlgen_python/describe_host_example.pyz '127.0.0.1:34953' Target Info Received: --- 127.0.0.1:34953 --- nodename: fuchsia-emulator product_config: core board_config: x64 version: 2025-04-08T02:04:13+00:00
To communicate with a Fuchsia component, a script must know the component's moniker in advance. A component moniker can be retrieved using ffx
. For example, the following ffx
command will print that core/build-info
exposes the fuchsia.buildinfo/Provider
capability:
ffx component capability fuchsia.buildinfo.Provider
This command will print output similar to the following:
Declarations: `core/build-info` declared capability `fuchsia.buildinfo.Provider` Exposes: `core/build-info` exposed `fuchsia.buildinfo.Provider` from self to parent Offers: `core` offered `fuchsia.buildinfo.Provider` from child `#build-info` to child `#cobalt` `core` offered `fuchsia.buildinfo.Provider` from child `#build-info` to child `#remote-control` `core` offered `fuchsia.buildinfo.Provider` from child `#build-info` to child `#sshd-host` `core` offered `fuchsia.buildinfo.Provider` from child `#build-info` to child `#test_manager` `core` offered `fuchsia.buildinfo.Provider` from child `#build-info` to child `#testing` `core` offered `fuchsia.buildinfo.Provider` from child `#build-info` to child `#toolbox` `core/sshd-host` offered `fuchsia.buildinfo.Provider` from parent to collection `#shell` Uses: `core/remote-control` used `fuchsia.buildinfo.Provider` from parent `core/sshd-host/shell:sshd-0` used `fuchsia.buildinfo.Provider` from parent `core/cobalt` used `fuchsia.buildinfo.Provider` from parent
This section demonstrates various other scripted interactions.
Note: For more information on Python FIDL bindings, see this Python FIDL bindings page.
There's more than one way to reboot a device. One approach to reboot a device is to connect to a component running the fuchsia.hardware.power.statecontrol/Admin
protocol, which can be found under /bootstrap/shutdown_shim
.
With this approach, the protocol is expected to exit mid-execution of the method with a PEER_CLOSED
error:
{% includecode gerrit_repo="fuchsia/fuchsia" gerrit_path="src/developer/ffx/lib/fuchsia-controller/end_to_end_tests/mobly/reboot_test.py" region_tag="reboot_example" %}
However, a challenging part comes afterward when you need to determine whether or not the device has come back online. This is usually done by attempting to connect to a protocol (usually the RemoteControl
protocol) until a timeout is reached.
Note: This section may be subject to change depending on the development in the component framework.
You can use the RemoteControl
protocol to start a component, which involves the following steps:
Connect to the lifecycle controller:
import fidl_fuchsia_developer_remotecontrol as f_remotecontrol import fidl_fuchsia_sys2 as f_sys2 ch = ctx.connect_to_remote_control_proxy() remote_control_proxy = f_remotecontrol.RemoteControlClient(ch) client, server = fuchsia_controller_py.Channel.create() await remote_control_proxy.root_lifecycle_controller(server=server.take()) lifecycle_ctrl = f_sys2.LifecycleControllerClient(client)
Attempt to start the instance of the component:
import fidl_fuchsia_component as f_component client, server = fuchsia_controller_py.Channel.create() await lifecycle_ctrl.start_instance("some_moniker", server=server.take()) binder = f_component.BinderClient(client)
The binder
object lets the user know whether or not the component remains connected. However, it has no methods. Support to determine whether the component has become unbound (using the binder protocol) is not yet implemented.
Getting a snapshot from a fuchsia device involves running a snapshot and binding a File
protocol for reading:
{% includecode gerrit_repo="fuchsia/fuchsia" gerrit_path="src/developer/ffx/lib/fuchsia-controller/end_to_end_tests/mobly/target_identity_tests.py" region_tag="snapshot_example" %}
An important task for Fuchsia Controller (either for handling passed bindings or for testing complex client side code) is to run a FIDL server. In this section, you return to the echo
example and implement an echo
server. The functions you need to override are derived from the FIDL file definition. So the echo
server (using the ffx
protocol) would look like below:
{% includecode gerrit_repo="fuchsia/fuchsia" gerrit_path="tools/fidl/fidlgen_python/tests/test_server_and_event_handler.py" region_tag="echo_server_impl" %}
To make a proper implementation, you need to import the appropriate libraries. As before, you will import fidl_fuchsia_developer_ffx
. However, since you're going to run an echo
server, the quickest way to test this server is to use a Channel
object from the fuchsia_controller_py
library:
import fidl_fuchsia_developer_ffx as ffx from fuchsia_controller_py import Channel
This Channel
object behaves similarly to the ones in other languages. The following code is a simple program that utilizes the echo
server:
import asyncio import unittest import fidl_fuchsia_developer_ffx as ffx from fuchsia_controller_py import Channel {% includecode gerrit_repo="fuchsia/fuchsia" gerrit_path="tools/fidl/fidlgen_python/tests/test_server_and_event_handler.py" region_tag="echo_server_impl" %} class TestCases(unittest.IsolatedAsyncioTestCase): async def test_echoer_example(self): {% includecode gerrit_repo="fuchsia/fuchsia" gerrit_path="tools/fidl/fidlgen_python/tests/test_server_and_event_handler.py" region_tag="use_echoer_example" %}
There are a few things to note when implementing a server:
sync
or async
.serve()
task will process requests and call the necessary method in the server implementation until either the task is completed or the underlying channel object is closed.PEER_CLOSED
error. Then you must check the result of the serving task.Note: For more information on writing async Python code with Fuchsia Controller, see this async Python page.
In contrast to the simple echo
server example above, this section covers different types of server interactions.
Let's work with the following FIDL protocol to make a server:
library fuchsia.exampleserver; type SomeGenericError = flexible enum { THIS = 1; THAT = 2; THOSE = 3; }; closed protocol Example { strict CheckFileExists(struct { path string:255; follow_symlinks bool; }) -> (struct { exists bool; }) error SomeGenericError; };
FIDL method names are derived by changing the method name from Camel case to Lower snake case. So the method CheckFileExists
in Python changes to check_file_exists
.
The anonymous struct types is derived from the whole protocol name and method. As a result, they can be quite verbose. The input method's input parameter is defined as a type called ExampleCheckFileExistsRequest
. And the response is called ExampleCheckFileExistsResponse
.
Putting these together, the FIDL server implementation in Python looks like below:
import fidl_fuchsia_exampleserver as fe class ExampleServerImpl(fe.ExampleServer): def some_file_check_function(path: str) -> bool: # Just pretend a real check happens here. return True def check_file_exists(self, req: fe.ExampleCheckFileExistsRequest) -> fe.ExampleCheckFileExistsResponse: return fe.ExampleCheckFileExistsResponse( exists=ExampleServerImpl.some_file_check_function() )
It is also possible to implement the methods as async
without issues.
In addition, returning an error requires wrapping the error in the FIDL DomainError
object, for example:
import fidl_fuchsia_exampleserver as fe from fidl import DomainError class ExampleServerImpl(fe.ExampleServer): def check_file_exists(self, req: fe.ExampleCheckFileExistsRequests) -> fe.ExampleCheckFileExistsResponse | DomainError: return DomainError(error=fe.SomeGenericError.THIS)
Event handlers are written similarly to servers. Events are handled on the client side of a channel, so passing a client is necessary to construct an event handler.
Let's start with the following FIDL code to build an example:
library fuchsia.exampleserver; closed protocol Example { strict -> OnFirst(struct { message string:128; }); strict -> OnSecond(); };
This FIDL example contains two different events that the event handler needs to handle. Writing the simplest class that does nothing but print looks like below:
import fidl_fuchsia_exampleserver as fe class ExampleEventHandler(fe.ExampleEventHandler): def on_first(self, req: fe.ExampleOnFirstRequest): print(f"Got a message on first: {req.message}") def on_second(self): print(f"Got an 'on second' event")
If you want to stop handling events without error, you can raise fidl.StopEventHandler
.
An example of this event can be tested using some existing fidlgen_python testing code. But first, make sure that the Fuchsia controller tests have been added to the Fuchsia build settings, for example:
fx set ... --with-host //tools/fidl/fidlgen_python:tests
With a protocol from fuchsia.controller.test
(defined in fuchsia_controller.test.fidl
), you can write code that uses the ExampleEvents
protocol, for example:
import asyncio import fidl_fuchsia_controller_test as fct from fidl import StopEventHandler from fuchsia_controller_py import Channel class ExampleEventHandler(fct.ExampleEventsEventHandler): def on_first(self, req: fct.ExampleEventsOnFirstRequest): print(f"Got on-first event message: {req.message}") def on_second(self): print(f"Got on-second event") raise StopEventHandler async def main(): client_chan, server_chan = Channel.create() client = fct.ExampleEventsClient(client_chan) server = fct.ExampleEventsServer(server_chan) event_handler = ExampleEventHandler(client) event_handler_task = asyncio.get_running_loop().create_task( event_handler.serve() ) server.on_first(message="first message") server.on_second() server.on_complete() await event_handler_task if __name__ == "__main__": asyncio.run(main())
Then this can be run by completing the Python environment setup steps in the next section. When run, it prints the following output and exits:
Got on-first event message: first message Got on-second event
For more examples on server testing, see this test_server_and_event_handler.py
file.