Write a service¶
Write a caproto IOC for a session to launch, declare it, and point a device
at it. Services explains what a service is and
how the container handles it.
Prerequisites¶
The epics extra, with caproto and ophyd-async's Channel Access support:
Write the IOC¶
Besides serving its process variables, a service redsun launches prints a
line when ready, stops when its standard input closes, and binds to the
loopback interface.
# mylab/iocs/camera.py
import signal
import sys
import threading
from caproto.server import PVGroup, ioc_arg_parser, pvproperty, run
def stop_when_stdin_closes() -> None:
sys.stdin.read()
signal.raise_signal(signal.SIGINT)
class Camera(PVGroup):
exposure = pvproperty(value=0.1, name="Exposure")
if __name__ == "__main__":
options, run_options = ioc_arg_parser(default_prefix="CAM:", desc="camera")
threading.Thread(target=stop_when_stdin_closes, daemon=True).start()
run(Camera(**options).pvdb, **{**run_options, "interfaces": ["127.0.0.1"]})
- The readiness line.
caprotoprintsServer startup complete.once it serves its process variables; wait for that line. - Standard input. Closing it is how the container asks a service to stop on
every platform. The watcher raises
SIGINT, so the service shuts down as on Ctrl+C and runscaproto's shutdown hooks. Use a daemon thread: a watcher run throughloop.run_in_executorleaves the process waiting on a thread still blocked on the read. - Cleanup before output. If the launching session dies, the service's input closes and nobody reads its output, so writing to standard output raises. Clean up first, then print, or do not print.
- Loopback. Binding to
127.0.0.1keeps a local service off the network. A service other machines attach to leavesinterfacesalone.
Declare it¶
In Python, on the container, beside the device that talks to it:
from redsun.containers import AppContainer, declare_device, declare_service
class MyApp(AppContainer):
camera_ioc = declare_service(
module="mylab.iocs.camera",
ready="Server startup complete.",
prefix="CAM:",
args=["--prefix", "CAM:"],
stop_timeout=30,
)
camera = declare_device(MyCamera, service="camera_ioc")
The device receives the service's prefix as its prefix keyword, so a device
also given prefix is refused. The build skips a device whose service did not
start.
In a plugin, the manifest gives the module and readiness line, and the session file names the plugin entry:
# mylab/redsun.yaml
services:
camera-ioc:
module: mylab.iocs.camera
ready: "Server startup complete."
# session.yaml
services:
camera_ioc:
plugin_name: mylab
plugin_id: camera-ioc
prefix: "CAM:"
args: ["--prefix", "CAM:"]
beamline:
prefix: "BL01:"
devices:
camera:
plugin_name: mylab
plugin_id: camera
service: camera_ioc
beamline has no module, so it is attached to: nothing starts or stops, and
its devices only receive its prefix.
React when it exits¶
A launched service exiting unasked emits sig_exited with its name and exit
code. Connect it in wire:
from redsun.containers import AppContainer, declare_presenter, declare_service
from redsun.log import Loggable
from redsun.presenter import Presenter
from redsun.virtual import slot
class CameraPresenter(Presenter, Loggable):
@slot(thread="main")
def on_service_exited(self, name: str, code: int) -> None:
self.logger.warning(f"{name} exited with code {code}")
class MyApp(AppContainer):
camera_ioc = declare_service(
module="mylab.iocs.camera", ready="Server startup complete."
)
presenter = declare_presenter(CameraPresenter)
def wire(self) -> None:
self.connect(self.camera_ioc.sig_exited, self.presenter.on_service_exited)
Without thread="main", a presenter slot runs on the thread reading the
service's output.
Log from it¶
The service's output is logged under redsun.service.<name>. To keep record
levels instead of every line at DEBUG, write JSON; see
Log from a service.