Skip to content

Commit 81f8fad

Browse files
authored
Add some docs (#338)
* Improve docstrings * Add datatype and transport explanations * Add some how-to guides * Add controllers explanation * Add projects using fastcs reference * Add what is fastcs explanation
1 parent a61df13 commit 81f8fad

37 files changed

Lines changed: 2018 additions & 29 deletions

docs/explanations/controllers.md

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
# Controllers
2+
3+
FastCS provides three controller classes: `Controller`, `ControllerVector`, and
4+
`BaseController`. This document explains what each does and when to use each.
5+
6+
## Controller
7+
8+
`Controller` is the primary building block for FastCS drivers. It can serve two roles:
9+
10+
**Root controller:** passed directly to the `FastCS` launcher. In this role, FastCS
11+
will call its lifecycle hooks and run the scan tasks it creates on the event loop.
12+
13+
**Sub controller:** attached to a parent controller via `add_sub_controller()` or by
14+
assigning it as an attribute. In this role, the sub controller's lifecycle hooks
15+
(`connect`, `reconnect`, `initialise`, `disconnect`) are not called automatically by
16+
FastCS. The parent controller is responsible for calling them as part of its own
17+
lifecycle, if required.
18+
19+
### Lifecycle hooks
20+
21+
| Method | Purpose |
22+
|---|---|
23+
| `initialise` | Dynamically add attributes on startup, before the API is built |
24+
| `connect` | Open connection to device |
25+
| `reconnect` | Re-open connection after scan error |
26+
| `disconnect` | Release device resources before shutdown |
27+
28+
### Scan task behaviour
29+
30+
When used as the root controller, FastCS collects all `@scan` methods and readable
31+
attributes with `update_period` set, across the whole controller hierarchy to be run as
32+
background tasks by FastCS. Scan tasks are gated on the `_connected` flag: if a scan
33+
raises an exception, `_connected` is set to `False` and tasks pause until `reconnect`
34+
sets it back to `True`.
35+
36+
```python
37+
from fastcs.controllers import Controller
38+
from fastcs.attributes import AttrR, AttrRW
39+
from fastcs.datatypes import Float, String
40+
from fastcs.methods import scan
41+
42+
43+
class TemperatureController(Controller):
44+
temperature = AttrR(Float(units="degC"))
45+
setpoint = AttrRW(Float(units="degC"))
46+
47+
async def connect(self):
48+
self._client = await DeviceClient.connect(self._host, self._port)
49+
self._connected = True
50+
51+
async def reconnect(self):
52+
try:
53+
self._client = await DeviceClient.connect(self._host, self._port)
54+
self._connected = True
55+
except Exception:
56+
logger.error("Failed to reconnect")
57+
58+
async def disconnect(self):
59+
await self._client.close()
60+
61+
@scan(period=1.0)
62+
async def update_temperature(self):
63+
value = await self._client.get_temperature()
64+
await self.temperature.update(value)
65+
```
66+
67+
### Using Controller as a sub controller
68+
69+
When a `Controller` is nested inside another, it organises the driver into logical
70+
sections and its attributes are exposed under a prefixed path. If the sub
71+
controller also has connection logic, the parent must invoke it explicitly:
72+
73+
```python
74+
class ChannelController(Controller):
75+
value = AttrR(Float())
76+
77+
async def connect(self):
78+
...
79+
self._connected = True
80+
81+
82+
class RootController(Controller):
83+
channel: ChannelController
84+
85+
def __init__(self):
86+
super().__init__()
87+
self.channel = ChannelController()
88+
89+
async def connect(self):
90+
await self.channel.connect()
91+
self._connected = True
92+
```
93+
94+
## ControllerVector
95+
96+
`ControllerVector` is a convenience wrapper for a set of controllers of the same type,
97+
distinguished by a non-contiguous integer index rather than a string name.
98+
99+
Children are accessed via `controller[<index>]` instead of `controller.<name>`. The type
100+
parameter `Controller_T` makes iteration type-safe when all children are the same
101+
concrete type: iterating yields `Controller_T` directly, with no `isinstance` checks
102+
needed. Mixing different subtypes is not prevented at runtime, but doing so widens the
103+
inferred type to the common base, losing the type-safety benefit.
104+
105+
```python
106+
from fastcs.controllers import Controller, ControllerVector
107+
108+
109+
class ChannelController(Controller):
110+
value = AttrR(Float())
111+
112+
113+
class RootController(Controller):
114+
channels: ControllerVector[ChannelController]
115+
116+
def __init__(self, num_channels: int):
117+
super().__init__()
118+
119+
self.channels = ControllerVector(
120+
{i: ChannelController() for i in range(num_channels)}
121+
)
122+
123+
async def connect(self):
124+
for channel in self.channels.values():
125+
await channel.connect()
126+
127+
self._connected = True
128+
129+
async def update_all(self):
130+
for index, channel in self.channels.items():
131+
value = await self._client.get_channel(index)
132+
await channel.value.update(value)
133+
```
134+
135+
Key properties of `ControllerVector`:
136+
137+
- Indexes are integers and do not need to be contiguous (e.g. `{1: ..., 3: ..., 7: ...}`)
138+
- All children must be `Controller` instances of the same type
139+
- Named sub controllers cannot be added to a `ControllerVector`
140+
- Children are exposed to transports with their integer index as the path component
141+
142+
### When to use ControllerVector instead of Controller
143+
144+
Use `ControllerVector` when:
145+
146+
- The device has a set of identical channels, axes, or modules identified by number
147+
- You need to iterate over sub controllers and perform the same action on each
148+
- The number of instances may vary (e.g. determined at runtime during `initialise`)
149+
150+
Use a plain `Controller` with named sub controllers when the sub controllers are
151+
distinct components with different types or roles.
152+
153+
## BaseController
154+
155+
`BaseController` is the common base class for both `Controller` and `ControllerVector`.
156+
It handles the creation and validation of attributes, scan methods, command methods, and
157+
sub controllers, including type hint introspection and IO connection.
158+
159+
`BaseController` is public for use in **type hints only**. It should not be subclassed
160+
directly when implementing a device driver. Use `Controller` or `ControllerVector`
161+
instead.
162+
163+
```python
164+
from fastcs.controllers import BaseController
165+
166+
167+
def configure_all(controller: BaseController) -> None:
168+
"""Accept any controller type for generic operations."""
169+
for name, attr in controller.attributes.items():
170+
...
171+
```

0 commit comments

Comments
 (0)