Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
5bb366c
Add Controller.id lifecycle for multi-controller foundation
gilesknap May 5, 2026
0d4a329
Add pv_prefix_from_path utility for path-based PV derivation
gilesknap May 5, 2026
893487d
Add validate_rest_id for REST controller id validation
gilesknap May 5, 2026
6dd956d
Unify Transport.connect signature on list[ControllerAPI]
gilesknap May 5, 2026
0b2e198
Route REST routes per controller id; reject illegal ids at connect
gilesknap May 5, 2026
011bb68
Add multi-class launch() with dict-by-id controllers schema
gilesknap May 5, 2026
d4ff267
Wire FastCS multi-controller end-to-end (REST)
gilesknap May 5, 2026
c8adb33
EPICS CA multi-root softioc with id-based PV prefix
gilesknap May 5, 2026
c1b95a2
Update EPICS multi-transport docs for id-based prefix
gilesknap May 5, 2026
4395605
EPICS PVA multi-root with N PVI roots
gilesknap May 5, 2026
5317473
Use literal markup for P4PIOC docstring refs
gilesknap May 5, 2026
e521778
GraphQL combined schema with id-keyed top-level Query fields
gilesknap May 5, 2026
fc8e710
GUI/docs emission: per-id files plus index file (#358)
gilesknap May 6, 2026
f2c7cef
Fix tutorial emphasize-lines after #358 snippet collapse
gilesknap May 6, 2026
f6600bc
Demo two controllers, rename controller.yaml -> fastcs.yaml, migratio…
gilesknap May 6, 2026
207d68d
Tango multi-device per controller with id in device name
gilesknap May 6, 2026
4de23e6
Flatten controllers entry: inline options fields next to type
gilesknap May 6, 2026
439f2fe
Make `type:` discriminator mandatory on every controllers entry
gilesknap May 6, 2026
7ab10fe
Use module-level registry instead of dynamic Entry-class attributes
gilesknap May 6, 2026
c8be8d6
Drop stale nitpick_ignore for controller_pv_prefix
gilesknap May 6, 2026
142e4e8
Decouple multi-controller doc example from demo
gilesknap May 6, 2026
d341fff
Call set_id on controllers in tutorial snippets
gilesknap May 6, 2026
2cf219b
Drop dead path guard in GraphQL transport connect
gilesknap May 6, 2026
f055141
Thread expects_options flag through entry registry
gilesknap May 6, 2026
04d36b2
Detect colliding Tango device-class names at connect
gilesknap May 6, 2026
c87711e
Hoist shared EPICS id-validation skeleton into common util
gilesknap May 6, 2026
6907741
Repoint EPICS_MAX_NAME_LENGTH consumers at canonical home
gilesknap May 6, 2026
b284afe
Drop dead EpicsDocs shim
gilesknap May 6, 2026
4853d0e
Use controller id verbatim in GUI index DeviceRef
gilesknap May 6, 2026
6db26cc
Fail fast on punctuation-only controller ids in EPICS GUI emission
gilesknap May 6, 2026
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
2 changes: 1 addition & 1 deletion .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
"module": "fastcs.demo",
"args": [
"run",
"${workspaceFolder:FastCS}/src/fastcs/demo/controller.yaml",
"${workspaceFolder:FastCS}/src/fastcs/demo/fastcs.yaml",
"--log-level",
"TRACE",
// "--graylog-endpoint",
Expand Down
1 change: 0 additions & 1 deletion docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,6 @@
("py:class", "fastcs.logging._graylog.GraylogStaticFields"),
("py:class", "fastcs.logging._graylog.GraylogEnvFields"),
("py:obj", "fastcs.control_system.build_controller_api"),
("py:obj", "fastcs.transports.epics.util.controller_pv_prefix"),
("docutils", "fastcs.demo.controllers.TemperatureControllerSettings"),
# TypeVar without docstrings still give warnings
("py:class", "strawberry.schema.schema.Schema"),
Expand Down
84 changes: 67 additions & 17 deletions docs/how-to/launch-framework.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,26 +54,75 @@ if __name__ == "__main__":

## YAML Configuration Files

Create a YAML configuration file matching the schema:
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:

```yaml
# device_config.yaml
controller:
ip_address: "192.168.1.100"
port: 25565
timeout: 10.0
# fastcs.yaml
controllers:
DEVICE:
type: DeviceController
ip_address: "192.168.1.100"
port: 25565
timeout: 10.0

transport:
- epicsca:
pv_prefix: "DEVICE"
- epicsca: {}
```

Every entry carries a required `type:` discriminator that names the
Controller class to instantiate. The remaining fields under each id
come straight from that class's `__init__` options type
(`DeviceSettings` here).

The key under `controllers:` (here `DEVICE`) is the controller id, used
verbatim as the EPICS PV prefix and as the REST route prefix.

Run with:

```bash
python my_driver.py run device_config.yaml
python my_driver.py run fastcs.yaml
```

### Hosting multiple controllers

`controllers:` is a dict, so a single application can host more than one
controller. Each entry needs a unique id (the dict key); the `type:`
discriminator selects which class to instantiate. For example, two
`DeviceController` instances on different IPs sharing a single transport
list:

```yaml
# fastcs.yaml
controllers:
MAIN:
type: DeviceController
ip_address: "192.168.1.100"
port: 25565
timeout: 10.0
AUX:
type: 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:
Expand All @@ -86,9 +135,11 @@ Use this schema for IDE autocompletion in YAML files:

```yaml
# yaml-language-server: $schema=schema.json
controller:
ip_address: "192.168.1.100"
# ... IDE will provide autocompletion
controllers:
DEVICE:
type: DeviceController
ip_address: "192.168.1.100"
# ... IDE will provide autocompletion
```

## Transport Configuration
Expand All @@ -98,10 +149,9 @@ Transports are configured in the `transport` section as a list:
```yaml
transport:
# EPICS Channel Access
- epicsca:
pv_prefix: "DEVICE"
- epicsca: {}
gui:
output_path: "opis/device.bob"
output_dir: "opis"
title: "Device Control"

# REST API
Expand All @@ -121,10 +171,10 @@ The `run` command includes logging options:

```bash
# Set log level
python my_driver.py run config.yaml --log-level debug
python my_driver.py run fastcs.yaml --log-level debug

# Send logs to Graylog
python my_driver.py run config.yaml \
python my_driver.py run fastcs.yaml \
--graylog-endpoint "graylog.example.com:12201" \
--graylog-static-fields "app=my_driver,env=prod"
```
Expand Down
118 changes: 118 additions & 0 deletions docs/how-to/migrate-to-multi-controller.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
# Migrate to Multi-Controller FastCS

FastCS now supports more than one top-level Controller per application.
The launch-framework config schema, the EPICS option dataclasses, and
the bundled demo all changed shape to accommodate this. This guide
covers the manual migration steps for an existing FastCS app.

## 1. Rename `controller.yaml` → `fastcs.yaml`

The bundled demo's config file moved from
`src/fastcs/demo/controller.yaml` to `src/fastcs/demo/fastcs.yaml`. The
name `fastcs.yaml` is now the recommended convention for application
configs, but the launcher does not hard-code it — `python -m my_driver
run <path>` still accepts any path. If you rely on the demo path
explicitly (e.g. in a `launch.json` debug config), update it.

## 2. `controller:` → `controllers: { <id>: ... }`

The top-level singular `controller:` block is gone. Replace it with a
dict keyed by controller id:

```yaml
# Before
controller:
ip_address: "192.168.1.100"
port: 25565

transport:
- epicsca: {}
```

```yaml
# After
controllers:
DEVICE: # id — used as the addressing prefix
type: DeviceController # required discriminator
ip_address: "192.168.1.100"
port: 25565

transport:
- epicsca: {}
```

The dict key (here `DEVICE`) is the controller id. It is used verbatim
as the EPICS PV prefix, the REST route prefix, the GraphQL top-level
Query field, and the Tango device-name segment. See
[Run Multiple Transports Simultaneously](multiple-transports.md) for
the per-transport id charset rules — GraphQL's `[A-Za-z_][A-Za-z0-9_]*`
is the lowest common denominator.

To host more than one controller, add more dict entries. Duplicate ids
are rejected at config-load time.

## 3. Drop `pv_prefix` from `EpicsIOCOptions`

`EpicsIOCOptions` and its `pv_prefix` field are removed. The PV prefix
is now derived from the controller id, so a transport block that used
to look like:

```yaml
# Before
transport:
- epicsca:
pv_prefix: DEVICE
```

becomes:

```yaml
# After
transport:
- epicsca: {}
```

The same applies to PVA. If you construct transports in Python rather
than via YAML, replace `EpicsCATransport(epicsca=EpicsIOCOptions(
pv_prefix="DEVICE"))` with `EpicsCATransport()` plus
`controller.set_id("DEVICE")` (or set the id from the YAML key when
using `launch()`).

## 4. `type:` discriminator is required on every entry

Each entry under `controllers:` carries a required `type:` discriminator
that names the Controller class to instantiate. The discriminator value
is the class `__name__`, or `type_name: ClassVar[str]` on the class if
set. The same rule applies whether `launch()` is called with a single
class or with several — `type:` is never optional.

```yaml
# Two-class app: launch([Lakeshore, Eurotherm])
controllers:
CRYO:
type: Lakeshore
ip_address: "192.168.1.100"
OVEN:
type: Eurotherm
ip_address: "192.168.1.101"

transport:
- epicsca: {}
```

## 5. Direct `FastCS(...)` usage is unchanged for the single-controller case

If you instantiate `FastCS` directly rather than via `launch()`, the
single-controller form `FastCS(controller, transports)` still works.
For multi-controller, pass a sequence:
`FastCS([controller_a, controller_b], transports)`. Each Controller
must have had `set_id(...)` called before being handed to `FastCS`.

## 6. GUI/docs emission output is now a directory

`EpicsGUIOptions.output_path` (single file) was renamed to
`output_dir` (directory). `EpicsDocsOptions.path` likewise renamed to
`output_dir`. Per-controller files (`<id>.bob`, `<id>.md`) plus an
`index.<ext>` are written into the directory — even when only one
controller is configured. Update any YAML or Python that set the old
field names.
Loading
Loading