Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions docs/explanations/controllers.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,9 @@ lifecycle, if required.
### Scan task behaviour

When used as the root controller, FastCS collects all `@scan` methods and readable
attributes with `update_period` set, across the whole controller hierarchy to be run as
background tasks by FastCS. Scan tasks are gated on the `_connected` flag: if a scan
attributes with a `getter` and a `poll_period` set, across the whole controller
hierarchy, to be run as background tasks by FastCS. Scan tasks are gated on the
`_connected` flag: if a scan
raises an exception, `_connected` is set to `False` and tasks pause until `reconnect`
sets it back to `True`.

Expand Down Expand Up @@ -154,7 +155,7 @@ distinct components with different types or roles.

`BaseController` is the common base class for both `Controller` and `ControllerVector`.
It handles the creation and validation of attributes, scan methods, command methods, and
sub controllers, including type hint introspection and IO connection.
sub controllers, including type hint introspection.

`BaseController` is public for use in **type hints only**. It should not be subclassed
directly when implementing a device driver. Use `Controller` or `ControllerVector`
Expand Down
8 changes: 4 additions & 4 deletions docs/explanations/datatypes.md
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,7 @@ float_type.validate(42) # Returns 42.0 (int -> float)
Validation runs automatically when:

1. **Attribute update**: `await attr.update(value)` validates before storing
2. **Put request**: `await attr.put(value)` validates before sending to device
2. **Set request**: `await attr.set(value)` validates before sending to device
3. **Initial value**: Values passed to `initial_value` are validated on creation

```python
Expand All @@ -188,9 +188,9 @@ attr = AttrRW(Int(min=0, max=10), initial_value=5)
await attr.update(7) # OK
await attr.update(15) # Raises ValueError

# Puts are validated
await attr.put(3) # OK
await attr.put(-1) # Raises ValueError
# Sets are validated
await attr.set(3) # OK
await attr.set(-1) # Raises ValueError
```

## Transport Handling
Expand Down
55 changes: 30 additions & 25 deletions docs/explanations/transports.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,9 +99,8 @@ layer.
| Callback | Registered with | Triggered By | Direction | Purpose |
|----------|-----------------|--------------|-----------|---------|
| On Update | `add_on_update_callback()` | `attr.update(value)` | Publish ↑ | Update protocol representation when attribute value changes |
| Sync Setpoint | `add_sync_setpoint_callback()` | `attr.put(value, sync_setpoint=True)` | Publish ↑ | Update transport's setpoint display without device communication |
| Update Datatype | `add_update_datatype_callback()` | `datatype` property changes | Publish ↑ | Update protocol metadata when datatype changes |
| Put | `attr.put(value)` | Transport receives user input | Put ↓ | Forward write requests from protocol to attribute |
| Set | `attr.set(value)` | Transport receives user input | Set ↓ | Forward write requests from protocol to attribute |

### On Update Callbacks

Expand Down Expand Up @@ -140,49 +139,55 @@ def create_read(name, attribute):

The callback receives the new `DataType` instance and should update the protocol's metadata representation (e.g., EPICS record fields like `EGU`, `HOPR`, `LOPR`).

### Put
### Set

When the transport receives a write request from the protocol, call `await
attribute.put(value)` to forward it to the attribute. This triggers validation and
propagates the value to the device via the IO layer. The transport should also update
its own setpoint display directly rather than relying on the sync setpoint callback
being called.
attribute.set(value)` to forward it to the attribute. This triggers validation, caches
the value as the attribute's `.setpoint`, and (if the attribute has one) runs its
`setter` to propagate the value to the device. If the setter returns a non-`None` value,
that becomes the attribute's new `.setpoint`/readback - the device's accepted or
clamped value. The transport should also update its own setpoint display directly
rather than relying on a callback.

```python
def create_write(name, attribute):
protocol_setpoint = Protocol(name)

async def handle_write(value):
protocol_setpoint.post(value)
await attribute.put(value)
await attribute.set(value)
```

### Sync Setpoint Callbacks
### Seeding a Setpoint Display from the Readback

Use `add_sync_setpoint_callback()` to update the protocol layer's setpoint
representation when the transport receives a write request. This is called when
`AttrW.put` is called with `sync_setpoint=True`.

Each transport is responsible for updating its own setpoint display while actioning the
change and should not rely on its sync setpoint callback being called by the attribute,
nor should it call `AttrW.put` with `sync_setpoint=True`. Setpoints should not be synced
between transports in this case - this is intentional to show which transport the change
came from.
An `AttrRW`'s setpoint starts out equal to its datatype's default, which may not match
the device's actual current value until the first poll happens. To avoid a write PV
briefly displaying a stale default, seed it once the first readback value is known,
using a one-shot `add_on_update_callback()` on the readback side:

```python
from fastcs.attributes import AttrR


def create_write(name, attribute):
protocol_setpoint = Protocol(name)

...

async def update_setpoint_display(value):
protocol_setpoint.post(value)
if isinstance(attribute, AttrR):
seeded = False

attribute.add_sync_setpoint_callback(update_setpoint_display)
```
async def seed_setpoint_once(value):
nonlocal seeded
if not seeded:
seeded = True
protocol_setpoint.post(value)

Sync setpoint callbacks are used in specific cases:
attribute.add_on_update_callback(seed_setpoint_once)
```

- When an attribute delegates to other attributes that actually communicate with the device
- During the first update of an `AttrRW`, to initialize the setpoint with the first readback value
This only applies to `AttrRW` (readable and writable) - a pure `AttrW` has no readback
to seed a setpoint display from.

## Commands

Expand Down
5 changes: 2 additions & 3 deletions docs/explanations/what-is-fastcs.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,8 @@ without modification.
A FastCS application has three layers:

**Controller** - a Python class that models the device. It holds attributes and
commands, implements connection logic, and creates periodic polling tasks. The
controller can create `AttributeIO`s to handle `update` and `send` operations between
attributes and the device.
commands, implements connection logic, and creates periodic polling tasks. Attributes
take `getter`/`setter` callables that read and write values on the device.

**Attributes and commands** - typed values (`AttrR`, `AttrW`, `AttrRW`) and callable
actions (`@command`) declared on the controller. Attributes represent the device's
Expand Down
2 changes: 1 addition & 1 deletion docs/how-to/table-waveform-data.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ await controller.channel_data.update(data)

```python
# Get the table
table = controller.results.get()
table = controller.results.readback

# Access by column name
names = table["name"]
Expand Down
Loading
Loading