Skip to content

Commit 5f7467c

Browse files
committed
Add some how-to guides
1 parent 9fcb6cf commit 5f7467c

9 files changed

Lines changed: 1081 additions & 0 deletions
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
# Arrange EPICS Screens with Groups and Sub-Controllers
2+
3+
This guide shows how to use `group` on attributes and commands to organise widgets into
4+
labelled boxes on a screen, and how splitting a device into sub-controllers creates
5+
navigable sub-screens for larger devices.
6+
7+
Both the CA and PVA EPICS transports generate screens from the same controller structure,
8+
so the techniques shown here apply to both.
9+
10+
## Group Attributes and Commands into Boxes
11+
12+
By default, all attributes and commands on a controller appear as a flat list of widgets
13+
on the generated screen. Assigning a `group` string places them together inside a labelled
14+
box.
15+
16+
```python
17+
from fastcs.attributes import AttrR, AttrRW
18+
from fastcs.controllers import Controller
19+
from fastcs.datatypes import Float, Int
20+
from fastcs.methods import command
21+
22+
23+
class PowerSupplyController(Controller):
24+
voltage = AttrRW(Float(), group="Output")
25+
current = AttrRW(Float(), group="Output")
26+
power = AttrR(Float(), group="Output")
27+
28+
temperature = AttrR(Float(), group="Status")
29+
fault_code = AttrR(Int(), group="Status")
30+
31+
@command(group="Actions")
32+
async def reset_faults(self) -> None:
33+
...
34+
35+
@command(group="Actions")
36+
async def enable_output(self) -> None:
37+
...
38+
```
39+
40+
The generated screen will show three boxes — **Output**, **Status**, and **Actions**
41+
each containing only the widgets assigned to that group. Attributes and commands with no
42+
`group` are placed outside any box, directly on the screen.
43+
44+
## Use Sub-Controllers to Create Sub-Screens
45+
46+
For devices with many attributes, a single flat screen becomes unwieldy. Splitting
47+
functionality across multiple controllers, connected with `add_sub_controller()`, causes
48+
the transport to generate a top-level screen with navigation links to per-sub-controller
49+
sub-screens.
50+
51+
```python
52+
from fastcs.attributes import AttrR, AttrRW
53+
from fastcs.controllers import Controller
54+
from fastcs.datatypes import Float, Int
55+
from fastcs.methods import command
56+
57+
58+
class ChannelController(Controller):
59+
voltage = AttrRW(Float(), group="Output")
60+
current = AttrRW(Float(), group="Output")
61+
temperature = AttrR(Float(), group="Status")
62+
63+
@command(group="Actions")
64+
async def enable(self) -> None:
65+
...
66+
67+
68+
class MultiChannelPSU(Controller):
69+
total_power = AttrR(Float())
70+
71+
@command()
72+
async def disable_all(self) -> None:
73+
...
74+
75+
def __init__(self, num_channels: int) -> None:
76+
super().__init__()
77+
for i in range(1, num_channels + 1):
78+
self.add_sub_controller(f"Ch{i:02d}", ChannelController())
79+
```
80+
81+
The top-level screen for `MultiChannelPSU` shows `TotalPower` and `DisableAll` alongside
82+
buttons labelled **Ch01**, **Ch02**, … that each open the sub-screen for that channel.
83+
Each channel sub-screen then shows the **Output**, **Status**, and **Actions** boxes
84+
defined on `ChannelController`.

docs/how-to/launch-framework.md

Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
# Use the Launch Framework for CLI Applications
2+
3+
This guide shows how to use `launch()` to create deployable FastCS drivers with
4+
automatic CLI generation and YAML configuration.
5+
6+
## Basic Setup
7+
8+
The `launch()` function generates a CLI from the controller's type hints:
9+
10+
```python
11+
from fastcs.controllers import Controller
12+
from fastcs.launch import launch
13+
14+
class MyController(Controller):
15+
pass
16+
17+
if __name__ == "__main__":
18+
launch(MyController)
19+
```
20+
21+
This creates a CLI with:
22+
23+
- `--version` - Display version information
24+
- `schema` - Output JSON schema for configuration
25+
- `run <config.yaml>` - Start the controller with a YAML config file
26+
27+
## Adding Configuration Options
28+
29+
It is recommended to use a dataclass or Pydantic model for the controller's
30+
configuration, as these provide schema generation and IDE support. The `launch()`
31+
function checks that `__init__` has at most one argument (besides `self`) and that the
32+
argument has a type hint, which is required to infer the schema:
33+
34+
```python
35+
from dataclasses import dataclass
36+
37+
from fastcs.controllers import Controller
38+
from fastcs.launch import launch
39+
40+
@dataclass
41+
class DeviceSettings:
42+
ip_address: str
43+
port: int = 25565
44+
timeout: float = 5.0
45+
46+
class DeviceController(Controller):
47+
def __init__(self, settings: DeviceSettings):
48+
super().__init__()
49+
self.settings = settings
50+
51+
if __name__ == "__main__":
52+
launch(DeviceController, version="1.0.0")
53+
```
54+
55+
## YAML Configuration Files
56+
57+
Create a YAML configuration file matching the schema:
58+
59+
```yaml
60+
# device_config.yaml
61+
controller:
62+
ip_address: "192.168.1.100"
63+
port: 25565
64+
timeout: 10.0
65+
66+
transport:
67+
- epicsca:
68+
pv_prefix: "DEVICE"
69+
```
70+
71+
Run with:
72+
73+
```bash
74+
python my_driver.py run device_config.yaml
75+
```
76+
77+
## Schema Generation
78+
79+
Generate JSON schema for the configuration yaml:
80+
81+
```bash
82+
python my_driver.py schema > schema.json
83+
```
84+
85+
Use this schema for IDE autocompletion in YAML files:
86+
87+
```yaml
88+
# yaml-language-server: $schema=schema.json
89+
controller:
90+
ip_address: "192.168.1.100"
91+
# ... IDE will provide autocompletion
92+
```
93+
94+
## Transport Configuration
95+
96+
Transports are configured in the `transport` section as a list:
97+
98+
```yaml
99+
transport:
100+
# EPICS Channel Access
101+
- epicsca:
102+
pv_prefix: "DEVICE"
103+
gui:
104+
output_path: "opis/device.bob"
105+
title: "Device Control"
106+
107+
# REST API
108+
- rest:
109+
host: "0.0.0.0"
110+
port: 8080
111+
112+
# GraphQL
113+
- graphql:
114+
host: "localhost"
115+
port: 8081
116+
```
117+
118+
## Logging Options
119+
120+
The `run` command includes logging options:
121+
122+
```bash
123+
# Set log level
124+
python my_driver.py run config.yaml --log-level debug
125+
126+
# Send logs to Graylog
127+
python my_driver.py run config.yaml \
128+
--graylog-endpoint "graylog.example.com:12201" \
129+
--graylog-static-fields "app=my_driver,env=prod"
130+
```
131+
132+
Available log levels: `TRACE`, `DEBUG`, `INFO`, `WARNING`, `ERROR`, `CRITICAL`
133+
134+
## Version Information
135+
136+
Pass a version string to display the driver version:
137+
138+
```python
139+
launch(DeviceController, version="1.2.3")
140+
```
141+
142+
```bash
143+
$ python my_driver.py --version
144+
DeviceController: 1.2.3
145+
FastCS: 0.12.0
146+
```
147+
148+
## Constraints
149+
150+
The `launch()` function requires:
151+
152+
1. Controller `__init__` must have at most 2 arguments (including `self`)
153+
2. If a configuration argument exists, it must have a type hint
154+
155+
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.
156+
157+
```python
158+
# Valid - no config
159+
class SimpleController(Controller):
160+
def __init__(self):
161+
super().__init__()
162+
163+
# Valid - with config
164+
class ConfiguredController(Controller):
165+
def __init__(self, settings: MySettings):
166+
super().__init__()
167+
168+
# Invalid - missing type hint
169+
class BadController(Controller):
170+
def __init__(self, settings): # Error: no type hint
171+
super().__init__()
172+
173+
# Invalid - too many arguments
174+
class TooManyArgs(Controller):
175+
def __init__(self, settings: MySettings, extra: str): # Error
176+
super().__init__()
177+
```

0 commit comments

Comments
 (0)