Skip to content

How a session runs and talks to services

A service is a server your devices talk to, such as an EPICS IOC, a camera server or a motion controller's gateway. Your ophyd-async devices talk to it over Channel Access or PVAccess without redsun taking part. redsun does the rest: it starts a launched service, notices when it exits, stops it cleanly, and hands its prefix and transport to the devices.

Devices without a service

You don't strictly need a service, since an ophyd-async device can reach its hardware on its own. For now, though, redsun supports such devices only in part, so a service is the preferred way to reach hardware. We will provide tutorials on wrapping third-party packages through ophyd-async in the future.

Devices and services

A session splits your setup into devices and services, which know as little as possible about each other. Point at a shape to read what it does:

Devices meet services at the prefix
the setup, as devicesthe hardware, as servicesmotor controllercamerastagex, yA device is a set of signals that presenters and plans read and set. It says what can be controlled, not how to reach the hardware.cameraexposure, roiA device is a set of signals that presenters and plans read and set. It says what can be controlled, not how to reach the hardware.stage serviceA service opens the serial port or the camera, speaks the vendor's protocol or runs the vendor's library, and offers the result as process variables. It knows nothing about the setup around it.camera serviceA service opens the serial port or the camera, speaks the vendor's protocol or runs the vendor's library, and offers the result as process variables. It knows nothing about the setup around it. prefix ST:prefix CAM:A device is a set of signals that presenters and plans read and set. It says what can be controlled, not how to reach the hardware. A device is a set of signals that presenters and plans read and set. It says what can be controlled, not how to reach the hardware. A service opens the serial port or the camera, speaks the vendor's protocol or runs the vendor's library, and offers the result as process variables. It knows nothing about the setup around it. A service opens the serial port or the camera, speaks the vendor's protocol or runs the vendor's library, and offers the result as process variables. It knows nothing about the setup around it.
the setup, as devicesthe hardware, as servicesmotor controllercamerastagex, yA device is a set of signals that presenters and plans read and set. It says what can be controlled, not how to reach the hardware.cameraexposure, roiA device is a set of signals that presenters and plans read and set. It says what can be controlled, not how to reach the hardware.stage serviceA service opens the serial port or the camera, speaks the vendor's protocol or runs the vendor's library, and offers the result as process variables. It knows nothing about the setup around it.camera serviceA service opens the serial port or the camera, speaks the vendor's protocol or runs the vendor's library, and offers the result as process variables. It knows nothing about the setup around it. prefix ST:prefix CAM:A device is a set of signals that presenters and plans read and set. It says what can be controlled, not how to reach the hardware. A device is a set of signals that presenters and plans read and set. It says what can be controlled, not how to reach the hardware. A service opens the serial port or the camera, speaks the vendor's protocol or runs the vendor's library, and offers the result as process variables. It knows nothing about the setup around it. A service opens the serial port or the camera, speaks the vendor's protocol or runs the vendor's library, and offers the result as process variables. It knows nothing about the setup around it.

The devices model your setup the way its users think of it, as an ophyd-async tree: a microscope has a stage, the stage an X and a Y axis, each axis a position. A service describes no setup; it only makes hardware reachable, so the same model can sit on real hardware or on a simulation.

A session can declare the whole setup as one device at the root of such a tree. MyMicroscope holds its stage and its camera, so the session declares only the microscope:

class MyMicroscope(StandardReadable):
    def __init__(
        self,
        stage_prefix: str,
        camera_prefix: str | None = None,
        name: str = "",
    ) -> None:
        self.xy = MyStage(stage_prefix)
        if camera_prefix is not None:
            self.camera = MyCamera(camera_prefix)
        super().__init__(name=name)


class MyApp(QtSession):
    microscope: Annotated[
        AsDevice[MyMicroscope],
        Declare(
            services={"stage_prefix": "stage_ioc", "camera_prefix": "camera_ioc"}
        ),
    ]

A child named stage stops the device

ophyd-async keeps the names of the bluesky methods, such as stage, read and set, so a child given one of them raises a NameError when the device is made. Name the stage after what it moves, as xy here.

The session then holds every device of the tree, under the name ophyd-async gives it, such as microscope-xy, so the positioner finds the stage's axes and a plan can be offered the camera. Asking about devices says how components see them. A device can also declare its children with annotations, in the ophyd-async style. An EpicsDevice creates them when it is made, or while it connects when it asks the IOC for its structure (with_pvi=True). The session reads the tree after connecting, so it finds them either way.

The pieces of a setup often sit behind different services, as the stage and the camera do in the picture above. So services maps each of the root's prefix arguments to the service that gives it: here stage_prefix gets the prefix of stage_ioc, and camera_prefix that of camera_ioc. A session file says the same under the device's entry:

devices:
  microscope:
    services:
      stage_prefix: stage_ioc
      camera_prefix: camera_ioc

An argument whose default is None makes its piece optional. When camera_ioc does not start, the session leaves camera_prefix out, builds the microscope with its stage alone, and says so in the build summary:

Built without: microscope (camera_prefix: camera_ioc not started)
Services not started: camera_ioc (exited with code 3 before it was ready)

An argument with any other default, or none, is required, so the session skips the whole device instead. A strict session stops in either case, because a service did not start. You can still declare each piece as a device of its own, and one session can mix both styles.

A device declared with service="stage_ioc" gets that service's prefix as its prefix argument, the same as services={"prefix": "stage_ioc"}, and the prefix is all it knows of the service. So you can change one side and keep the other:

  • The same components run against the lab's services or simulated ones; only the services change:

    # common.yaml: what the setup is
    devices:
      stage:
        plugin_name: mylab
        plugin_id: stage
        service: stage_ioc
    
    # lab.yaml: how it is reached in the lab
    services:
      stage_ioc:
        plugin_name: mylab
        plugin_id: stage-ioc
        prefix: "ST:"
    
    # simulation.yaml: how it is reached without hardware
    services:
      stage_ioc:
        plugin_name: mylab
        plugin_id: stage-sim
        prefix: "ST:"
    

    The session lists common.yaml and one of the other two in its config.

  • A crashing vendor library takes down its service, not the session; the service's devices time out until it is back.

  • An attached service can run on another machine, since the devices need only its prefix.

A device that needs no hardware, such as the soft stage in the tutorial, needs no service.

Two connection levels

A device reaches its hardware in two steps, and each step connects on its own:

The two connection levels

A device reaches its hardware in two levels, and each one connects on its own.

session processviewpresenterdevice
session processviewpresenterdevice

Level 1: the build calls connect() on every device declared with autoconnect, all at once. A device that hasn't connected after 10 seconds is left out.

session processservice processor containerviewpresenterdeviceThe build calls connect() on every device declared with autoconnect, all at once. A device that hasn't connected after 10 seconds is left out, like one that fails to build.service The build calls connect() on every device declared with autoconnect, all at once. A device that hasn't connected after 10 seconds is left out, like one that fails to build.
session processservice processor containerviewpresenterdeviceThe build calls connect() on every device declared with autoconnect, all at once. A device that hasn't connected after 10 seconds is left out, like one that fails to build.service The build calls connect() on every device declared with autoconnect, all at once. A device that hasn't connected after 10 seconds is left out, like one that fails to build.

Level 2: the service opens the hardware its process variables name, such as a serial port or a camera, on the machine it runs on.

session processservice processor containerviewpresenterdeviceThe build calls connect() on every device declared with autoconnect, all at once. A device that hasn't connected after 10 seconds is left out, like one that fails to build.serviceThe service opens a serial port or a camera. Which one it opens is set through its process variables.hardware The build calls connect() on every device declared with autoconnect, all at once. A device that hasn't connected after 10 seconds is left out, like one that fails to build. The service opens a serial port or a camera. Which one it opens is set through its process variables.
session processservice processor containerviewpresenterdeviceThe build calls connect() on every device declared with autoconnect, all at once. A device that hasn't connected after 10 seconds is left out, like one that fails to build.serviceThe service opens a serial port or a camera. Which one it opens is set through its process variables.hardware The build calls connect() on every device declared with autoconnect, all at once. A device that hasn't connected after 10 seconds is left out, like one that fails to build. The service opens a serial port or a camera. Which one it opens is set through its process variables.

A mocked session connects each device to a simulated backend and launches no service, so it reaches neither level. The two levels are kept apart on purpose: a session can stay connected to a service that holds no hardware yet, and a service can release its hardware while every connection stays up.

Launched and attached services

A service is either launched by the session or attached to it. You declare a launched one with Launch, naming the module the session runs, and an attached one with Attach, naming only its prefix:

class MyApp(QtSession):
    camera_ioc: Annotated[
        AsService, Launch("mylab.iocs.camera", ready="Server startup complete.")
    ]
    beamline: Annotated[AsService, Attach("BL01:", address="10.0.0.5")]

An attached service already runs, in a container or on another host; the session only passes its prefix on, and address says where to look when the network search wouldn't find it. Either kind can also be declared in a session file's services section, which fills any keyword the class leaves out; see Write a service.

A launched service starts in the first build step, before any device:

Starting a launched service
sessionservice processdevices python -m mylab.iocs.cameraprints the ready line,within startup_timeoutbuild, each with itsservice's prefixconnect()
sessionservice processdevices python -m mylab.iocs.cameraprints the ready line,within startup_timeoutbuild, each with itsservice's prefixconnect()

All launched services start at once, each waited for up to startup_timeout seconds, STARTUP_TIMEOUT (15) unless the declaration gives another; one declared without ready text counts as ready when its process starts. A service that doesn't start is logged, and every device naming it is skipped. Services stop at shutdown, after every component and before the log files close, so the files record how each ended; a build that raises stops the ones it started.

Stopping a process

The session asks a service to stop more and more firmly. After each step it waits up to stop_timeout seconds, 10 unless the declaration gives another, for the service to exit:

How the session stops a service

The session asks a service to stop more and more firmly, waiting stop_timeout seconds, 10 unless the declaration says otherwise, after each request.

close itsstandard inputsend SIGINTkill it still running,on POSIXstill runningstill running,on Windows
close itsstandard inputsend SIGINTkill it still running,on POSIXstill runningstill running,on Windows

First it closes the service's standard input. A service that watches its input cleans up and exits, on every platform.

close itsstandard inputA service watching its standard input cleans up and exits.send SIGINTkill it still running,on POSIXstill runningstill running,on WindowsA service watching its standard input cleans up and exits.
close itsstandard inputA service watching its standard input cleans up and exits.send SIGINTkill it still running,on POSIXstill runningstill running,on WindowsA service watching its standard input cleans up and exits.

On POSIX, a service still running then gets SIGINT.

close itsstandard inputsend SIGINTOnly on POSIX. On Windows, the session kills a service still running after the first step.kill it still running,on POSIXstill runningstill running,on WindowsOnly on POSIX. On Windows, the session kills a service still running after the first step.
close itsstandard inputsend SIGINTOnly on POSIX. On Windows, the session kills a service still running after the first step.kill it still running,on POSIXstill runningstill running,on WindowsOnly on POSIX. On Windows, the session kills a service still running after the first step.

A service still running after that, or on Windows after the first request, is killed, and may leave work such as a large file unfinished.

close itsstandard inputsend SIGINTkill itA killed service may leave work unfinished, typically a large file it was writing. still running,on POSIXstill runningstill running,on WindowsA killed service may leave work unfinished, typically a large file it was writing.
close itsstandard inputsend SIGINTkill itA killed service may leave work unfinished, typically a large file it was writing. still running,on POSIXstill runningstill running,on WindowsA killed service may leave work unfinished, typically a large file it was writing.

Closing standard input comes first because it is the only request that runs a service's cleanup on every platform: Popen.terminate() skips cleanup on Windows and Linux, and a console control event never reaches a process started without a console window, as a Qt application starts its services. It also stops a service when the session crashes, since the operating system closes the pipe. A service that needs longer to close takes a longer stop_timeout.

One transport per session

All the services of a session are reached over the same protocol, which you name once in the services section of the configuration:

services:
  transport: pv-access
  camera_ioc:
    plugin_name: mylab
    plugin_id: camera-ioc

A session speaks channel-access unless it says otherwise, and a file layered over another can't change it. Pick a scenario to compare how each transport sets up the launched services:

What the transport sets up

The transport, named once in the services section, decides how the session sets up each launched service.

session processcamera_iocstage_ioc looks at127.0.0.1:5101looks at127.0.0.1:5102
session processcamera_iocstage_ioc looks at127.0.0.1:5101looks at127.0.0.1:5102

Channel Access: each launched service gets a server port of its own, and the session adds each port to its own process's address list.

session processThe session gives each launched service a server port of its own, and adds each port to the address list of its own process.camera_iocown port 5101stage_iocown port 5102 looks at127.0.0.1:5101looks at127.0.0.1:5102The session gives each launched service a server port of its own, and adds each port to the address list of its own process.
session processThe session gives each launched service a server port of its own, and adds each port to the address list of its own process.camera_iocown port 5101stage_iocown port 5102 looks at127.0.0.1:5101looks at127.0.0.1:5102The session gives each launched service a server port of its own, and adds each port to the address list of its own process.

PVAccess: each launched service binds 127.0.0.1 on any free port, and the session tells its own process to search 127.0.0.1.

session processThe session binds each launched service to 127.0.0.1 and lets it take any free port, and tells its own process to look at 127.0.0.1.camera_ioc127.0.0.1, free portstage_ioc127.0.0.1, free port looks at127.0.0.1looks at127.0.0.1The session binds each launched service to 127.0.0.1 and lets it take any free port, and tells its own process to look at 127.0.0.1.
session processThe session binds each launched service to 127.0.0.1 and lets it take any free port, and tells its own process to look at 127.0.0.1.camera_ioc127.0.0.1, free portstage_ioc127.0.0.1, free port looks at127.0.0.1looks at127.0.0.1The session binds each launched service to 127.0.0.1 and lets it take any free port, and tells its own process to look at 127.0.0.1.

The port numbers are examples: the session takes free ones. Environment variables lists what it sets.

There is one transport because both protocols read variables that hold one setting for the whole process. Channel Access services get a port each because of Windows, where a second IOC on the default port is never found: libca reads its list of server addresses once per process, so each service keeps its port for as long as the session process runs. PVAccess servers share the search port, and the client is told to look at 127.0.0.1 because it doesn't search the loopback address otherwise.

Under sharedbox the session sets up nothing, because a device finds its service's boxes by name, under the service's prefix, with no port or address involved. The service and the session then have to run on the same computer. How to serve a FastCS controller over sharedbox shows both sides.

Another program can take the port first

A port is free when the session chooses it, but nothing holds it until the service binds it, so another program on the host can take it in between. If that happens, the service fails to start or its devices don't connect. The service keeps its port for the life of the session process, so to get a different port, start the session again in a new process.

redsun itself depends on ophyd-async and on nothing that either network protocol needs. Each component brings what its own service speaks, such as caproto, p4p or fastcs, plus ophyd-async[ca] or ophyd-async[pva] for the device side; see Write a service. The sharedbox transport comes with the sharedbox extra of redsun.

What a launched service receives

A launched service is told its name and prefix, its ready text and the session's log level, on top of the transport's variables. A Python service reads them with identity, ready and configure_logging, so one module can serve several sessions and name its channels from its identity. Environment variables lists how they are passed.

Unexpected exits

Nothing restarts a launched service that stops on its own. Step through what happens instead:

A launched service exits on its own

camera_ioc runs, and the camera's devices read and write through it.

camera devicesanswercamera_iocrunning reads, writes
camera devicesanswercamera_iocrunning reads, writes

camera_ioc exits on its own, after it was ready and without the session asking it to stop.

camera devicesanswercamera_iocexited reads, writes
camera devicesanswercamera_iocexited reads, writes

The session logs the exit code with the last 20 lines the service printed, and emits sig_exited with the service's name and code.

camera devicesanswercamera_iocexitedERROR in the logThe session logs the exit code and the last 20 lines the service printed.sig_exited(name, code) reads, writesThe session logs the exit code and the last 20 lines the service printed.
camera devicesanswercamera_iocexitedERROR in the logThe session logs the exit code and the last 20 lines the service printed.sig_exited(name, code) reads, writesThe session logs the exit code and the last 20 lines the service printed.

Connected slots run: a view's on the main thread in a Qt session, a presenter's on the thread that read the service's output unless it names another.

camera devicesanswercamera_iocexitedERROR in the logThe session logs the exit code and the last 20 lines the service printed.sig_exited(name, code)view slotmain threadIn a Qt session, the slots of a view that is a widget run on the main thread.presenter slotthread service-camera_iocA presenter slot runs on the thread that read the service's output, unless you choose another thread for it. reads, writesThe session logs the exit code and the last 20 lines the service printed. In a Qt session, the slots of a view that is a widget run on the main thread. A presenter slot runs on the thread that read the service's output, unless you choose another thread for it.
camera devicesanswercamera_iocexitedERROR in the logThe session logs the exit code and the last 20 lines the service printed.sig_exited(name, code)view slotmain threadIn a Qt session, the slots of a view that is a widget run on the main thread.presenter slotthread service-camera_iocA presenter slot runs on the thread that read the service's output, unless you choose another thread for it. reads, writesThe session logs the exit code and the last 20 lines the service printed. In a Qt session, the slots of a view that is a widget run on the main thread. A presenter slot runs on the thread that read the service's output, unless you choose another thread for it.

Nothing restarts the service, so every read or write of its devices times out after 10 seconds.

camera devicesTimeoutError after 10 scamera_iocexitedERROR in the logThe session logs the exit code and the last 20 lines the service printed.sig_exited(name, code)view slotmain threadIn a Qt session, the slots of a view that is a widget run on the main thread.presenter slotthread service-camera_iocA presenter slot runs on the thread that read the service's output, unless you choose another thread for it. reads, writesThe session logs the exit code and the last 20 lines the service printed. In a Qt session, the slots of a view that is a widget run on the main thread. A presenter slot runs on the thread that read the service's output, unless you choose another thread for it.
camera devicesTimeoutError after 10 scamera_iocexitedERROR in the logThe session logs the exit code and the last 20 lines the service printed.sig_exited(name, code)view slotmain threadIn a Qt session, the slots of a view that is a widget run on the main thread.presenter slotthread service-camera_iocA presenter slot runs on the thread that read the service's output, unless you choose another thread for it. reads, writesThe session logs the exit code and the last 20 lines the service printed. In a Qt session, the slots of a view that is a widget run on the main thread. A presenter slot runs on the thread that read the service's output, unless you choose another thread for it.

Once you start the service again, its devices answer again: both protocols reconnect on their own.

camera devicesanswer againBoth protocols reconnect on their own, so the devices need no reconnect.camera_iocstarted againERROR in the logThe session logs the exit code and the last 20 lines the service printed.sig_exited(name, code)view slotmain threadIn a Qt session, the slots of a view that is a widget run on the main thread.presenter slotthread service-camera_iocA presenter slot runs on the thread that read the service's output, unless you choose another thread for it. reads, writesBoth protocols reconnect on their own, so the devices need no reconnect. The session logs the exit code and the last 20 lines the service printed. In a Qt session, the slots of a view that is a widget run on the main thread. A presenter slot runs on the thread that read the service's output, unless you choose another thread for it.
camera devicesanswer againBoth protocols reconnect on their own, so the devices need no reconnect.camera_iocstarted againERROR in the logThe session logs the exit code and the last 20 lines the service printed.sig_exited(name, code)view slotmain threadIn a Qt session, the slots of a view that is a widget run on the main thread.presenter slotthread service-camera_iocA presenter slot runs on the thread that read the service's output, unless you choose another thread for it. reads, writesBoth protocols reconnect on their own, so the devices need no reconnect. The session logs the exit code and the last 20 lines the service printed. In a Qt session, the slots of a view that is a widget run on the main thread. A presenter slot runs on the thread that read the service's output, unless you choose another thread for it.

sig_exited is emitted only when the service exits after it was ready and without being asked to stop, from the thread reading its output, service-<name>. A presenter slot that touches what only the main thread may use, such as a Qt widget, needs @slot(thread="main"). An attached service has no process to watch, so an outage shows up only as timeouts on its devices.

Service output

Everything a launched service prints ends up in the logs, under the logger redsun.service.<name> and in the service's own log file, next to the application's:

Where a line of service output goes
a line theservice printsJSON log recordkeeps its level and timeA record written by loguru with serialize=True, or an object with the fields of a logging.LogRecord. It keeps its traceback, and its logger name under the service's logger.pvxs linekeeps its level and timeThe text pvxs, the library under a PVAccess server, writes: a time, a level, a logger and a message.any other linelogged at DEBUGSuch as a print, logged under the service's logger.the service'slog file A record written by loguru with serialize=True, or an object with the fields of a logging.LogRecord. It keeps its traceback, and its logger name under the service's logger. The text pvxs, the library under a PVAccess server, writes: a time, a level, a logger and a message. Such as a print, logged under the service's logger.
a line theservice printsJSON log recordkeeps its level and timeA record written by loguru with serialize=True, or an object with the fields of a logging.LogRecord. It keeps its traceback, and its logger name under the service's logger.pvxs linekeeps its level and timeThe text pvxs, the library under a PVAccess server, writes: a time, a level, a logger and a message.any other linelogged at DEBUGSuch as a print, logged under the service's logger.the service'slog file A record written by loguru with serialize=True, or an object with the fields of a logging.LogRecord. It keeps its traceback, and its logger name under the service's logger. The text pvxs, the library under a PVAccess server, writes: a time, a level, a logger and a message. Such as a print, logged under the service's logger.

Log from a service shows how a service writes records the session can read.

Not supported yet

  • Restarting a crashed service.
  • Standby, a service releasing its hardware while it stays connected. For now the presenter that owns the devices does it, see Letting go of hardware; the session will take it over once ophyd-async can disconnect a device.
  • Launching a service in a container. An attached service covers one you start beside the session with docker compose.

ADR 12 records the decisions behind this design.