Skip to content
Merged
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
4 changes: 2 additions & 2 deletions .github/workflows/_dist.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,6 @@ jobs:
with:
pip-install: dist/*.whl

- name: Test module --version works using the installed wheel
- name: Test module __version__ works using the installed wheel
# If more than one module in src/ replace with module name to test
run: python -m $(ls --hide='*.egg-info' src | head -1) --version
run: python -c 'from fastcs import __version__; print(__version__)'
3 changes: 3 additions & 0 deletions .github/workflows/_test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@ jobs:
python-version: ${{ inputs.python-version }}
pip-install: ".[dev]"

- name: Update setuptools
run: pip install --upgrade setuptools

- name: Run tests
run: tox -e tests

Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
/*bob

# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
Expand Down
23 changes: 23 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,29 @@
// Enable break on exception when debugging tests (see: tests/conftest.py)
"PYTEST_RAISE": "1",
},
},
{
"name": "Temp Controller Sim",
"type": "debugpy",
"request": "launch",
"module": "tickit",
"justMyCode": false,
"console": "integratedTerminal",
"args": [
"--log-level",
"INFO",
"all",
"${workspaceFolder:FastCS}/src/fastcs/demo/simulation/temp_controller.yaml"
]
},
{
"name": "Temp Controller FastCS",
"type": "debugpy",
"request": "launch",
"justMyCode": false,
"module": "fastcs.demo",
"args": ["run", "${workspaceFolder:FastCS}/src/fastcs/demo/controller.yaml"],
"console": "integratedTerminal",
}
]
}
2 changes: 2 additions & 0 deletions codecov.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
ignore:
- "src/fastcs/demo/"
5 changes: 5 additions & 0 deletions docs/_static/custom.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
/* allow a wider screen so we can fit 88 chars of source code on it */
.bd-page-width {
max-width: 100rem;
/* default is 88rem */
}
14 changes: 12 additions & 2 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@
"sphinx_design",
# So we can write markdown files
"myst_parser",
# Collapsible sections
"sphinx_togglebutton",
]

# So we can use the ::: syntax
Expand Down Expand Up @@ -91,7 +93,11 @@
("py:class", "fastcs.datatypes.T_Numerical"),
("py:class", "strawberry.schema.schema.Schema"),
]
nitpick_ignore_regex = [("py:class", "fastcs.*.T")]
nitpick_ignore_regex = [
("py:class", "fastcs.*.T"),
(r"py:.*", r"fastcs\.demo.*"),
(r"py:.*", r"tickit.*"),
]

# Both the class’ and the __init__ method’s docstring are concatenated and
# inserted into the main body of the autoclass directive
Expand Down Expand Up @@ -209,5 +215,9 @@
html_show_copyright = False

# Logo
html_logo = "images/dls-logo.svg"
html_logo = "images/fastcs.svg"
html_favicon = html_logo

# Custom CSS
html_static_path = ["_static"]
html_css_files = ["custom.css"]
119 changes: 119 additions & 0 deletions docs/snippets/dynamic.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
from __future__ import annotations

import json
from dataclasses import dataclass
from typing import Any, Literal

from pydantic import BaseModel, ConfigDict, ValidationError

from fastcs.attributes import AttrHandlerRW, Attribute, AttrR, AttrRW, AttrW
from fastcs.connections import IPConnection, IPConnectionSettings
from fastcs.controller import Controller, SubController
from fastcs.datatypes import Bool, DataType, Float, Int, String
from fastcs.launch import FastCS
from fastcs.transport.epics.ca.options import EpicsCAOptions
from fastcs.transport.epics.options import EpicsIOCOptions


class TemperatureControllerParameter(BaseModel):
model_config = ConfigDict(extra="forbid")

command: str
type: Literal["bool", "int", "float", "str"]
access_mode: Literal["r", "rw"]

@property
def fastcs_datatype(self) -> DataType:
match self.type:
case "bool":
return Bool()
case "int":
return Int()
case "float":
return Float()
case "str":
return String()


def create_attributes(parameters: dict[str, Any]) -> dict[str, Attribute]:
attributes: dict[str, Attribute] = {}
for name, parameter in parameters.items():
name = name.replace(" ", "_").lower()

try:
parameter = TemperatureControllerParameter.model_validate(parameter)
except ValidationError as e:
print(f"Failed to validate parameter '{parameter}'\n{e}")
continue

handler = TemperatureControllerHandler(parameter.command)
match parameter.access_mode:
case "r":
attributes[name] = AttrR(parameter.fastcs_datatype, handler=handler)
case "rw":
attributes[name] = AttrRW(parameter.fastcs_datatype, handler=handler)

return attributes


@dataclass
class TemperatureControllerHandler(AttrHandlerRW):
command_name: str
update_period: float | None = 0.2
_controller: TemperatureController | None = None

async def update(self, attr: AttrR):
response = await self.controller.connection.send_query(
f"{self.command_name}?\r\n"
)
value = response.strip("\r\n")

await attr.set(attr.dtype(value))

async def put(self, attr: AttrW, value: Any):
await self.controller.connection.send_command(
f"{self.command_name}={value}\r\n"
)


class TemperatureRampController(SubController):
def __init__(self, index: int, connection: IPConnection):
super().__init__(f"Ramp {index}")

self.connection = connection

async def initialise(self, parameters: dict[str, Any]):
self.attributes.update(create_attributes(parameters))


class TemperatureController(Controller):
def __init__(self, settings: IPConnectionSettings):
super().__init__()

self._ip_settings = settings
self.connection = IPConnection()

async def connect(self):
await self.connection.connect(self._ip_settings)

async def initialise(self):
await self.connect()

api = json.loads((await self.connection.send_query("API?\r\n")).strip("\r\n"))

ramps_api = api.pop("Ramps")
self.attributes.update(create_attributes(api))

for idx, ramp_parameters in enumerate(ramps_api):
ramp_controller = TemperatureRampController(idx + 1, self.connection)
self.register_sub_controller(f"Ramp{idx + 1:02d}", ramp_controller)
await ramp_controller.initialise(ramp_parameters)

await self.connection.close()


epics_options = EpicsCAOptions(ioc=EpicsIOCOptions(pv_prefix="DEMO"))
connection_settings = IPConnectionSettings("localhost", 25565)
fastcs = FastCS(TemperatureController(connection_settings), [epics_options])

# fastcs.run() # Commented as this will block
5 changes: 5 additions & 0 deletions docs/snippets/static01.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from fastcs.controller import Controller


class TemperatureController(Controller):
pass
10 changes: 10 additions & 0 deletions docs/snippets/static02.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
from fastcs.controller import Controller
from fastcs.launch import FastCS


class TemperatureController(Controller):
pass


fastcs = FastCS(TemperatureController(), [])
fastcs.run()
12 changes: 12 additions & 0 deletions docs/snippets/static03.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
from fastcs.attributes import AttrR
from fastcs.controller import Controller
from fastcs.datatypes import String
from fastcs.launch import FastCS


class TemperatureController(Controller):
device_id = AttrR(String())


fastcs = FastCS(TemperatureController(), [])
fastcs.run()
16 changes: 16 additions & 0 deletions docs/snippets/static04.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
from fastcs.attributes import AttrR
from fastcs.controller import Controller
from fastcs.datatypes import String
from fastcs.launch import FastCS
from fastcs.transport.epics.ca.options import EpicsCAOptions
from fastcs.transport.epics.options import EpicsIOCOptions


class TemperatureController(Controller):
device_id = AttrR(String())


epics_options = EpicsCAOptions(ioc=EpicsIOCOptions(pv_prefix="DEMO"))
fastcs = FastCS(TemperatureController(), [epics_options])

# fastcs.run() # Commented as this will block
27 changes: 27 additions & 0 deletions docs/snippets/static05.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
from fastcs.attributes import AttrR
from fastcs.connections import IPConnection, IPConnectionSettings
from fastcs.controller import Controller
from fastcs.datatypes import String
from fastcs.launch import FastCS
from fastcs.transport.epics.ca.options import EpicsCAOptions
from fastcs.transport.epics.options import EpicsIOCOptions


class TemperatureController(Controller):
device_id = AttrR(String())

def __init__(self, settings: IPConnectionSettings):
super().__init__()

self._ip_settings = settings
self.connection = IPConnection()

async def connect(self):
await self.connection.connect(self._ip_settings)


epics_options = EpicsCAOptions(ioc=EpicsIOCOptions(pv_prefix="DEMO"))
connection_settings = IPConnectionSettings("localhost", 25565)
fastcs = FastCS(TemperatureController(connection_settings), [epics_options])

# fastcs.run() # Commented as this will block
54 changes: 54 additions & 0 deletions docs/snippets/static06.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
from __future__ import annotations

from dataclasses import dataclass

from fastcs.attributes import AttrHandlerR, AttrR
from fastcs.connections import IPConnection, IPConnectionSettings
from fastcs.controller import BaseController, Controller
from fastcs.datatypes import String
from fastcs.launch import FastCS
from fastcs.transport.epics.ca.options import EpicsCAOptions
from fastcs.transport.epics.options import EpicsIOCOptions


@dataclass
class IDUpdater(AttrHandlerR):
update_period: float | None = 0.2
_controller: TemperatureController | None = None

async def initialise(self, controller: BaseController):
assert isinstance(controller, TemperatureController)
self._controller = controller

@property
def controller(self) -> TemperatureController:
if self._controller is None:
raise RuntimeError("Handler not initialised")

return self._controller

async def update(self, attr: AttrR):
response = await self.controller.connection.send_query("ID?\r\n")
value = response.strip("\r\n")

await attr.set(value)


class TemperatureController(Controller):
device_id = AttrR(String(), handler=IDUpdater())

def __init__(self, settings: IPConnectionSettings):
super().__init__()

self._ip_settings = settings
self.connection = IPConnection()

async def connect(self):
await self.connection.connect(self._ip_settings)


epics_options = EpicsCAOptions(ioc=EpicsIOCOptions(pv_prefix="DEMO"))
connection_settings = IPConnectionSettings("localhost", 25565)
fastcs = FastCS(TemperatureController(connection_settings), [epics_options])

# fastcs.run() # Commented as this will block
Loading
Loading