Use the Launch Framework for CLI Applications#
This guide shows how to use launch() to create deployable FastCS drivers with
automatic CLI generation and YAML configuration.
Basic Setup#
The launch() function generates a CLI from the controller’s type hints:
from fastcs.controllers import Controller
from fastcs.launch import launch
class MyController(Controller):
pass
if __name__ == "__main__":
launch(MyController)
This creates a CLI with:
--version- Display version informationschema- Output JSON schema for configurationrun <config.yaml>- Start the controller with a YAML config file
Adding Configuration Options#
It is recommended to use a dataclass or Pydantic model for the controller’s
configuration, as these provide schema generation and IDE support. The launch()
function checks that __init__ has at most one argument (besides self) and that the
argument has a type hint, which is required to infer the schema:
from dataclasses import dataclass
from fastcs.controllers import Controller
from fastcs.launch import launch
@dataclass
class DeviceSettings:
ip_address: str
port: int = 25565
timeout: float = 5.0
class DeviceController(Controller):
def __init__(self, settings: DeviceSettings):
super().__init__()
self.settings = settings
if __name__ == "__main__":
launch(DeviceController, version="1.0.0")
YAML Configuration Files#
Create a YAML configuration file matching the schema. The conventional
filename is fastcs.yaml, but any filename works — run takes the path
as an argument:
# fastcs.yaml
controllers:
- id: DEVICE
type: my_driver.DeviceController
ip_address: "192.168.1.100"
port: 25565
timeout: 10.0
transport:
- epicsca: {}
Every entry carries a required id: plus a required type: discriminator
that names the Controller class to instantiate. The default discriminator
is <top-level-package>.<ClassName> — using the package qualifier means
two independently-distributed packages (e.g. fastcs_eiger.Detector and
fastcs_pmac.Detector) can register classes with the same short name
without colliding. A class can opt out by setting
type_name: ClassVar[str] on itself, in which case that value is used
verbatim with no prefix added. The remaining fields under each entry come
straight from that class’s __init__ options type (DeviceSettings
here).
The id: (here DEVICE) is used verbatim as the EPICS PV prefix and as
the REST route prefix.
Run with:
python my_driver.py run fastcs.yaml
Hosting multiple controllers#
controllers: is a list, so a single application can host more than one
controller. Each entry needs a unique id:; the type: discriminator
selects which class to instantiate. For example, two DeviceController
instances on different IPs sharing a single transport list:
# fastcs.yaml
controllers:
- id: MAIN
type: my_driver.DeviceController
ip_address: "192.168.1.100"
port: 25565
timeout: 10.0
- id: AUX
type: my_driver.DeviceController
ip_address: "192.168.1.101"
port: 25565
timeout: 10.0
transport:
- epicsca: {}
When more than one class is registered with launch([ClassA, ClassB]),
each entry’s type: selects between them.
For a real working example, see src/fastcs/demo/fastcs.yaml, which
hosts two TemperatureController instances and can be run with
python -m fastcs.demo run src/fastcs/demo/fastcs.yaml.
The transport list is shared across all controllers: each transport sees the full set, and uses the per-entry id as the addressing prefix (EPICS PV prefix, REST route prefix, GraphQL top-level Query field, Tango device name segment).
Schema Generation#
Generate JSON schema for the configuration yaml:
python my_driver.py schema > schema.json
Use this schema for IDE autocompletion in YAML files:
# yaml-language-server: $schema=schema.json
controllers:
- id: DEVICE
type: my_driver.DeviceController
ip_address: "192.168.1.100"
# ... IDE will provide autocompletion
Transport Configuration#
Transports are configured in the transport section as a list:
transport:
# EPICS Channel Access
- epicsca: {}
gui:
output_dir: "opis"
title: "Device Control"
# REST API
- rest:
host: "0.0.0.0"
port: 8080
# GraphQL
- graphql:
host: "localhost"
port: 8081
Logging Options#
The run command includes logging options:
# Set log level
python my_driver.py run fastcs.yaml --log-level debug
# Send logs to Graylog
python my_driver.py run fastcs.yaml \
--graylog-endpoint "graylog.example.com:12201" \
--graylog-static-fields "app=my_driver,env=prod"
Available log levels: TRACE, DEBUG, INFO, WARNING, ERROR, CRITICAL
Version Information#
Pass a version string to display the driver version:
launch(DeviceController, version="1.2.3")
$ python my_driver.py --version
DeviceController: 1.2.3
FastCS: 0.12.0
Constraints#
The launch() function requires:
Controller
__init__must have at most 2 arguments (includingself)If a configuration argument exists, it must have a type hint
Using a dataclass or Pydantic model is recommended for the configuration type, as it enables JSON schema generation. Other type-hinted types will work, but will not produce a useful schema.
# Valid - no config
class SimpleController(Controller):
def __init__(self):
super().__init__()
# Valid - with config
class ConfiguredController(Controller):
def __init__(self, settings: MySettings):
super().__init__()
# Invalid - missing type hint
class BadController(Controller):
def __init__(self, settings): # Error: no type hint
super().__init__()
# Invalid - too many arguments
class TooManyArgs(Controller):
def __init__(self, settings: MySettings, extra: str): # Error
super().__init__()