Skip to content

Commit cdc2dcf

Browse files
committed
also added epics name options to the gui
1 parent 9229f70 commit cdc2dcf

File tree

4 files changed

+96
-45
lines changed

4 files changed

+96
-45
lines changed

src/fastcs/backends/epics/backend.py

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,19 +11,22 @@ def __init__(
1111
self,
1212
controller: Controller,
1313
pv_prefix: str = "MY-DEVICE-PREFIX",
14-
options: EpicsIOCOptions | None = None,
14+
ioc_options: EpicsIOCOptions | None = None,
1515
):
1616
super().__init__(controller)
1717

1818
self._pv_prefix = pv_prefix
19-
options = options or EpicsIOCOptions()
20-
self._ioc = EpicsIOC(pv_prefix, self._mapping, options=options)
19+
self.ioc_options = ioc_options or EpicsIOCOptions()
20+
self._ioc = EpicsIOC(pv_prefix, self._mapping, options=ioc_options)
2121

22-
def create_docs(self, options: EpicsDocsOptions | None = None) -> None:
23-
EpicsDocs(self._mapping).create_docs(options)
22+
def create_docs(self, docs_options: EpicsDocsOptions | None = None) -> None:
23+
EpicsDocs(self._mapping).create_docs(docs_options)
2424

25-
def create_gui(self, options: EpicsGUIOptions | None = None) -> None:
26-
EpicsGUI(self._mapping, self._pv_prefix).create_gui(options)
25+
def create_gui(self, gui_options: EpicsGUIOptions | None = None) -> None:
26+
assert self.ioc_options.name_options is not None
27+
EpicsGUI(
28+
self._mapping, self._pv_prefix, self.ioc_options.name_options
29+
).create_gui(gui_options)
2730

2831
def _run(self):
2932
self._ioc.run(self._dispatcher, self._context)

src/fastcs/backends/epics/gui.py

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
from pydantic import ValidationError
2828

2929
from fastcs.attributes import Attribute, AttrR, AttrRW, AttrW
30+
from fastcs.backends.epics.util import EpicsNameOptions, _convert_attr_name_to_pv_name
3031
from fastcs.cs_methods import Command
3132
from fastcs.datatypes import Bool, Float, Int, String
3233
from fastcs.exceptions import FastCSException
@@ -39,21 +40,41 @@ class EpicsGUIFormat(Enum):
3940
edl = ".edl"
4041

4142

42-
@dataclass
43+
@dataclass(frozen=True)
4344
class EpicsGUIOptions:
4445
output_path: Path = Path.cwd() / "output.bob"
4546
file_format: EpicsGUIFormat = EpicsGUIFormat.bob
4647
title: str = "Simple Device"
4748

4849

4950
class EpicsGUI:
50-
def __init__(self, mapping: Mapping, pv_prefix: str) -> None:
51+
def __init__(
52+
self,
53+
mapping: Mapping,
54+
pv_prefix: str,
55+
epics_name_options: EpicsNameOptions | None = None,
56+
) -> None:
5157
self._mapping = mapping
5258
self._pv_prefix = pv_prefix
59+
self.epics_name_options = epics_name_options or EpicsNameOptions()
5360

5461
def _get_pv(self, attr_path: list[str], name: str):
55-
attr_prefix = ":".join([self._pv_prefix] + attr_path)
56-
return f"{attr_prefix}:{name.title().replace('_', '')}"
62+
return self.epics_name_options.pv_separator.join(
63+
[
64+
self._pv_prefix,
65+
]
66+
+ [
67+
_convert_attr_name_to_pv_name(
68+
attr_name, self.epics_name_options.pv_naming_convention
69+
)
70+
for attr_name in attr_path
71+
]
72+
+ [
73+
_convert_attr_name_to_pv_name(
74+
name, self.epics_name_options.pv_naming_convention
75+
),
76+
],
77+
)
5778

5879
@staticmethod
5980
def _get_read_widget(attribute: AttrR) -> ReadWidgetUnion:

src/fastcs/backends/epics/ioc.py

Lines changed: 30 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
from collections.abc import Callable
22
from dataclasses import dataclass
3-
from enum import Enum
43
from types import MethodType
54
from typing import Any, Literal
65

@@ -11,6 +10,8 @@
1110
from fastcs.attributes import AttrR, AttrRW, AttrW
1211
from fastcs.backends.epics.util import (
1312
MBB_STATE_FIELDS,
13+
EpicsNameOptions,
14+
PvNamingConvention,
1415
attr_is_enum,
1516
enum_index_to_value,
1617
enum_value_to_index,
@@ -23,20 +24,10 @@
2324
EPICS_MAX_NAME_LENGTH = 60
2425

2526

26-
class PvNamingConvention(Enum):
27-
NO_CONVERSION = "NO_CONVERSION"
28-
PASCAL = "PASCAL"
29-
CAPITALIZED = "CAPITALIZED"
30-
31-
32-
DEFAULT_PV_SEPARATOR = ":"
33-
34-
35-
@dataclass
27+
@dataclass(frozen=True)
3628
class EpicsIOCOptions:
3729
terminal: bool = True
38-
pv_naming_convention: PvNamingConvention = PvNamingConvention.PASCAL
39-
pv_separator: str = DEFAULT_PV_SEPARATOR
30+
name_options: EpicsNameOptions = EpicsNameOptions()
4031

4132

4233
def _convert_attr_name_to_pv_name(
@@ -53,8 +44,10 @@ class EpicsIOC:
5344
def __init__(
5445
self, pv_prefix: str, mapping: Mapping, options: EpicsIOCOptions | None = None
5546
):
56-
self.options = options or EpicsIOCOptions()
57-
_add_pvi_info(f"{pv_prefix}{self.options.pv_separator}PVI")
47+
self._options = options or EpicsIOCOptions()
48+
self._name_options = self._options.name_options
49+
50+
_add_pvi_info(f"{pv_prefix}{self._name_options.pv_separator}PVI")
5851
self._add_sub_controller_pvi_info(pv_prefix, mapping.controller)
5952

6053
self._create_and_link_attribute_pvs(pv_prefix, mapping)
@@ -68,7 +61,7 @@ def run(
6861
builder.LoadDatabase()
6962
softioc.iocInit(dispatcher)
7063

71-
if self.options.terminal:
64+
if self._options.terminal:
7265
softioc.interactive_ioc(context)
7366

7467
def _add_sub_controller_pvi_info(self, pv_prefix: str, parent: BaseController):
@@ -79,14 +72,16 @@ def _add_sub_controller_pvi_info(self, pv_prefix: str, parent: BaseController):
7972
parent: Controller to add PVI refs for
8073
8174
"""
82-
parent_pvi = self.options.pv_separator.join([pv_prefix] + parent.path + ["PVI"])
75+
parent_pvi = self._name_options.pv_separator.join(
76+
[pv_prefix] + parent.path + ["PVI"]
77+
)
8378

8479
for child in parent.get_sub_controllers().values():
85-
child_pvi = self.options.pv_separator.join(
80+
child_pvi = self._name_options.pv_separator.join(
8681
[pv_prefix]
8782
+ [
8883
_convert_attr_name_to_pv_name(
89-
path, self.options.pv_naming_convention
84+
path, self._name_options.pv_naming_convention
9085
)
9186
for path in child.path
9287
]
@@ -101,18 +96,20 @@ def _add_sub_controller_pvi_info(self, pv_prefix: str, parent: BaseController):
10196
def _create_and_link_attribute_pvs(self, pv_prefix: str, mapping: Mapping) -> None:
10297
for single_mapping in mapping.get_controller_mappings():
10398
formatted_path = [
104-
_convert_attr_name_to_pv_name(p, self.options.pv_naming_convention)
99+
_convert_attr_name_to_pv_name(
100+
p, self._name_options.pv_naming_convention
101+
)
105102
for p in single_mapping.controller.path
106103
]
107104
for attr_name, attribute in single_mapping.attributes.items():
108105
pv_name = _convert_attr_name_to_pv_name(
109-
attr_name, self.options.pv_naming_convention
106+
attr_name, self._name_options.pv_naming_convention
110107
)
111-
_pv_prefix = self.options.pv_separator.join(
108+
_pv_prefix = self._name_options.pv_separator.join(
112109
[pv_prefix] + formatted_path
113110
)
114111
full_pv_name_length = len(
115-
f"{_pv_prefix}{self.options.pv_separator}{pv_name}"
112+
f"{_pv_prefix}{self._name_options.pv_separator}{pv_name}"
116113
)
117114

118115
if full_pv_name_length > EPICS_MAX_NAME_LENGTH:
@@ -163,7 +160,7 @@ async def async_record_set(value: T):
163160
record.set(value)
164161

165162
record = _get_input_record(
166-
f"{pv_prefix}{self.options.pv_separator}{pv_name}", attribute
163+
f"{pv_prefix}{self._name_options.pv_separator}{pv_name}", attribute
167164
)
168165
self._add_attr_pvi_info(record, pv_prefix, attr_name, "r")
169166

@@ -172,18 +169,20 @@ async def async_record_set(value: T):
172169
def _create_and_link_command_pvs(self, pv_prefix: str, mapping: Mapping) -> None:
173170
for single_mapping in mapping.get_controller_mappings():
174171
formatted_path = [
175-
_convert_attr_name_to_pv_name(p, self.options.pv_naming_convention)
172+
_convert_attr_name_to_pv_name(
173+
p, self._name_options.pv_naming_convention
174+
)
176175
for p in single_mapping.controller.path
177176
]
178177
for attr_name, method in single_mapping.command_methods.items():
179178
pv_name = _convert_attr_name_to_pv_name(
180-
attr_name, self.options.pv_naming_convention
179+
attr_name, self._name_options.pv_naming_convention
181180
)
182-
_pv_prefix = self.options.pv_separator.join(
181+
_pv_prefix = self._name_options.pv_separator.join(
183182
[pv_prefix] + formatted_path
184183
)
185184
if (
186-
len(f"{_pv_prefix}{self.options.pv_separator}{pv_name}")
185+
len(f"{_pv_prefix}{self._name_options.pv_separator}{pv_name}")
187186
> EPICS_MAX_NAME_LENGTH
188187
):
189188
print(
@@ -221,7 +220,7 @@ async def async_write_display(value: T):
221220
record.set(value, process=False)
222221

223222
record = _get_output_record(
224-
f"{pv_prefix}{self.options.pv_separator}{pv_name}",
223+
f"{pv_prefix}{self._name_options.pv_separator}{pv_name}",
225224
attribute,
226225
on_update=on_update,
227226
)
@@ -237,7 +236,7 @@ async def wrapped_method(_: Any):
237236
await method()
238237

239238
record = builder.aOut(
240-
f"{pv_prefix}{self.options.pv_separator}{pv_name}",
239+
f"{pv_prefix}{self._name_options.pv_separator}{pv_name}",
241240
initial_value=0,
242241
always_update=True,
243242
on_update=wrapped_method,
@@ -264,7 +263,7 @@ def _add_attr_pvi_info(
264263
record.add_info(
265264
"Q:group",
266265
{
267-
f"{prefix}{self.options.pv_separator}PVI": {
266+
f"{prefix}{self._name_options.pv_separator}PVI": {
268267
f"value.{name}.{access_mode}": {
269268
"+channel": "NAME",
270269
"+type": "plain",

src/fastcs/backends/epics/util.py

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
from dataclasses import dataclass
2+
from enum import Enum
3+
14
from fastcs.attributes import Attribute
25
from fastcs.datatypes import String, T
36

@@ -25,6 +28,31 @@
2528
MBB_MAX_CHOICES = len(_MBB_FIELD_PREFIXES)
2629

2730

31+
class PvNamingConvention(Enum):
32+
NO_CONVERSION = "NO_CONVERSION"
33+
PASCAL = "PASCAL"
34+
CAPITALIZED = "CAPITALIZED"
35+
36+
37+
DEFAULT_PV_SEPARATOR = ":"
38+
39+
40+
@dataclass(frozen=True)
41+
class EpicsNameOptions:
42+
pv_naming_convention: PvNamingConvention = PvNamingConvention.PASCAL
43+
pv_separator: str = DEFAULT_PV_SEPARATOR
44+
45+
46+
def _convert_attr_name_to_pv_name(
47+
attr_name: str, naming_convention: PvNamingConvention
48+
) -> str:
49+
if naming_convention == PvNamingConvention.PASCAL:
50+
return attr_name.title().replace("_", "")
51+
elif naming_convention == PvNamingConvention.CAPITALIZED:
52+
return attr_name.upper().replace("_", "-")
53+
return attr_name
54+
55+
2856
def attr_is_enum(attribute: Attribute) -> bool:
2957
"""Check if the `Attribute` has a `String` datatype and has `allowed_values` set.
3058
@@ -36,9 +64,9 @@ def attr_is_enum(attribute: Attribute) -> bool:
3664
3765
"""
3866
match attribute:
39-
case Attribute(
40-
datatype=String(), allowed_values=allowed_values
41-
) if allowed_values is not None and len(allowed_values) <= MBB_MAX_CHOICES:
67+
case Attribute(datatype=String(), allowed_values=allowed_values) if (
68+
allowed_values is not None and len(allowed_values) <= MBB_MAX_CHOICES
69+
):
4270
return True
4371
case _:
4472
return False

0 commit comments

Comments
 (0)