Skip to content

Commit f64645f

Browse files
committed
WIP: working ioc
1 parent 88f80a2 commit f64645f

File tree

9 files changed

+280
-5
lines changed

9 files changed

+280
-5
lines changed

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ dependencies = [
1919
"pytango",
2020
"softioc>=4.5.0",
2121
"strawberry-graphql",
22+
"p4p"
2223
]
2324
dynamic = ["version"]
2425
license.file = "LICENSE"

src/fastcs/launch.py

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import asyncio
22
import inspect
33
import json
4+
import signal
45
from pathlib import Path
56
from typing import Annotated, Any, Optional, TypeAlias, get_type_hints
67

@@ -16,12 +17,13 @@
1617
from .transport.adapter import TransportAdapter
1718
from .transport.epics.options import EpicsOptions
1819
from .transport.graphQL.options import GraphQLOptions
20+
from .transport.p4p.options import P4POptions
1921
from .transport.rest.options import RestOptions
2022
from .transport.tango.options import TangoOptions
2123

2224
# Define a type alias for transport options
2325
TransportOptions: TypeAlias = list[
24-
EpicsOptions | TangoOptions | RestOptions | GraphQLOptions
26+
EpicsOptions | TangoOptions | RestOptions | GraphQLOptions | P4POptions
2527
]
2628

2729

@@ -32,6 +34,7 @@ def __init__(
3234
transport_options: TransportOptions,
3335
):
3436
self._loop = asyncio.get_event_loop()
37+
self._loop.set_debug(True)
3538
self._backend = Backend(controller, self._loop)
3639
transport: TransportAdapter
3740
self._transports: list[TransportAdapter] = []
@@ -67,6 +70,13 @@ def __init__(
6770
controller,
6871
option,
6972
)
73+
case P4POptions():
74+
from .transport.p4p.adapter import P4PTransport
75+
76+
transport = P4PTransport(
77+
controller,
78+
option,
79+
)
7080
self._transports.append(transport)
7181

7282
def create_docs(self) -> None:
@@ -80,14 +90,19 @@ def create_gui(self) -> None:
8090
transport.create_gui()
8191

8292
def run(self):
83-
self._loop.run_until_complete(
84-
self.serve(),
85-
)
93+
serve = asyncio.ensure_future(self.serve())
94+
95+
self._loop.add_signal_handler(signal.SIGINT, serve.cancel)
96+
self._loop.add_signal_handler(signal.SIGTERM, serve.cancel)
97+
self._loop.run_until_complete(serve)
8698

8799
async def serve(self) -> None:
88100
coros = [self._backend.serve()]
89101
coros.extend([transport.serve() for transport in self._transports])
90-
await asyncio.gather(*coros)
102+
try:
103+
await asyncio.gather(*coros)
104+
except asyncio.CancelledError:
105+
pass
91106

92107

93108
def launch(

src/fastcs/transport/p4p/__init__.py

Whitespace-only changes.
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
from fastcs.controller import Controller
2+
from fastcs.transport.adapter import TransportAdapter
3+
4+
from .ioc import P4PIOC
5+
from .options import P4POptions
6+
7+
8+
class P4PTransport(TransportAdapter):
9+
def __init__(
10+
self,
11+
controller: Controller,
12+
options: P4POptions | None = None,
13+
) -> None:
14+
self._controller = controller
15+
self._options = options or P4POptions()
16+
self._pv_prefix = self.options.ioc.pv_prefix
17+
self._ioc = P4PIOC(self.options.ioc.pv_prefix, controller)
18+
19+
@property
20+
def options(self) -> P4POptions:
21+
return self._options
22+
23+
async def serve(self) -> None:
24+
await self._ioc.run()
25+
26+
def create_docs(self) -> None:
27+
raise NotImplementedError
28+
29+
def create_gui(self) -> None:
30+
raise NotImplementedError

src/fastcs/transport/p4p/ioc.py

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
import asyncio
2+
import time
3+
4+
from p4p.server import Server, StaticProvider
5+
from p4p.server.asyncio import SharedPV
6+
7+
from fastcs.attributes import Attribute, AttrR, AttrRW, AttrW
8+
from fastcs.controller import Controller
9+
10+
from .util import cast_from_p4p_type, cast_to_p4p_type, get_nt_from_datatype
11+
12+
_pvs = set()
13+
14+
15+
class AttrWHandler:
16+
def __init__(self, attr_w: AttrW | AttrRW):
17+
self._attr_w = attr_w
18+
19+
async def put(self, pv, op):
20+
value = op.value()
21+
raw_value = value.raw.value
22+
23+
await self._attr_w.process_without_display_update(
24+
cast_from_p4p_type(self._attr_w.datatype, raw_value)
25+
)
26+
27+
pv.post(value, timestamp=time.time())
28+
op.done()
29+
30+
31+
def make_shared_pv(attribute: Attribute) -> SharedPV:
32+
kwargs = {
33+
"nt": get_nt_from_datatype(attribute.datatype),
34+
"initial": cast_to_p4p_type(
35+
attribute.datatype, attribute.datatype.initial_value
36+
),
37+
"timestamp": time.time(),
38+
}
39+
if type(attribute) is AttrW or type(attribute) is AttrRW:
40+
kwargs["handler"] = AttrWHandler(attribute)
41+
42+
shared_pv = SharedPV(**kwargs)
43+
44+
if type(attribute) is AttrR or type(attribute) is AttrRW:
45+
shared_pv.post(
46+
cast_to_p4p_type(attribute.datatype, attribute.get()), timestamp=time.time()
47+
)
48+
49+
async def on_update(value):
50+
shared_pv.post(
51+
cast_to_p4p_type(attribute.datatype, value), timestamp=time.time()
52+
)
53+
54+
attribute.set_update_callback(on_update)
55+
56+
return shared_pv
57+
58+
59+
async def make_attribute_providers(
60+
prefix_root: str, controller: Controller
61+
) -> dict[str, StaticProvider]:
62+
providers = {}
63+
64+
for single_mapping in controller.get_controller_mappings():
65+
path = single_mapping.controller.path
66+
pv_prefix = ":".join([prefix_root] + path)
67+
provider = StaticProvider(pv_prefix)
68+
providers[pv_prefix] = provider
69+
70+
for attr_name, attribute in single_mapping.attributes.items():
71+
pv_name = f"{pv_prefix}:{attr_name.title().replace('_', '')}"
72+
73+
shared_pv = make_shared_pv(attribute)
74+
75+
_pvs.add(shared_pv)
76+
provider.add(pv_name, shared_pv)
77+
78+
return providers
79+
80+
81+
class P4PIOC:
82+
def __init__(
83+
self,
84+
pv_prefix: str,
85+
controller: Controller,
86+
):
87+
self.pv_prefix = pv_prefix
88+
self.controller = controller
89+
90+
async def run(self):
91+
"""To be ran in the same event loop as `self.loop`."""
92+
93+
"""
94+
if asyncio.get_running_loop() is not self.loop:
95+
raise RuntimeError("Must run in the same event loop as `self.loop`")
96+
"""
97+
98+
providers = await make_attribute_providers(self.pv_prefix, self.controller)
99+
100+
endless_event = asyncio.Event()
101+
with Server(providers.values()):
102+
await endless_event.wait()
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
from dataclasses import dataclass, field
2+
3+
4+
@dataclass
5+
class P4PIOCOptions:
6+
pv_prefix: str = "MY-DEVICE-PREFIX"
7+
8+
9+
@dataclass
10+
class P4POptions:
11+
ioc: P4PIOCOptions = field(default_factory=P4PIOCOptions)

src/fastcs/transport/p4p/util.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
from p4p.nt import NTEnum, NTScalar
2+
3+
from fastcs.datatypes import Bool, DataType, Enum, Float, Int, String, T
4+
5+
P4P_ALLOWED_DATATYPES = (Int, Float, String, Bool, Enum)
6+
7+
8+
def get_nt_from_datatype(datatype: DataType):
9+
match datatype:
10+
case Int():
11+
return NTScalar("i")
12+
case Float():
13+
return NTScalar("d")
14+
case String():
15+
return NTScalar("s")
16+
case Bool():
17+
return NTScalar("b")
18+
case Enum():
19+
return NTEnum()
20+
case _:
21+
raise RuntimeError(f"Datatype `{datatype}` unsupported in P4P.")
22+
23+
24+
def cast_from_p4p_type(datatype: DataType[T], value: object) -> T:
25+
match datatype:
26+
case Enum():
27+
return datatype.validate(datatype.members[value.index])
28+
case datatype if issubclass(type(datatype), P4P_ALLOWED_DATATYPES):
29+
return datatype.validate(value) # type: ignore
30+
case _:
31+
raise ValueError(f"Unsupported datatype {datatype}")
32+
33+
34+
def cast_to_p4p_type(datatype: DataType[T], value: T) -> object:
35+
match datatype:
36+
case Enum():
37+
return {
38+
"choices": [member.name for member in datatype.members],
39+
"index": datatype.index_of(value),
40+
}
41+
case datatype if issubclass(type(datatype), P4P_ALLOWED_DATATYPES):
42+
return datatype.validate(value) # type: ignore
43+
case _:
44+
raise ValueError(f"Unsupported datatype {datatype}")

tests/data/schema.json

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,26 @@
109109
"title": "GraphQLServerOptions",
110110
"type": "object"
111111
},
112+
"P4PIOCOptions": {
113+
"properties": {
114+
"pv_prefix": {
115+
"default": "MY-DEVICE-PREFIX",
116+
"title": "Pv Prefix",
117+
"type": "string"
118+
}
119+
},
120+
"title": "P4PIOCOptions",
121+
"type": "object"
122+
},
123+
"P4POptions": {
124+
"properties": {
125+
"ioc": {
126+
"$ref": "#/$defs/P4PIOCOptions"
127+
}
128+
},
129+
"title": "P4POptions",
130+
"type": "object"
131+
},
112132
"RestOptions": {
113133
"properties": {
114134
"rest": {
@@ -202,6 +222,9 @@
202222
},
203223
{
204224
"$ref": "#/$defs/GraphQLOptions"
225+
},
226+
{
227+
"$ref": "#/$defs/P4POptions"
205228
}
206229
]
207230
},

tests/p4p_ioc.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import enum
2+
3+
from fastcs.attributes import AttrR, AttrRW, AttrW
4+
from fastcs.controller import Controller, SubController
5+
from fastcs.datatypes import Bool, Enum, Int
6+
from fastcs.launch import FastCS
7+
from fastcs.transport.p4p.options import P4PIOCOptions, P4POptions
8+
from fastcs.wrappers import command, scan
9+
10+
11+
class FEnum(enum.Enum):
12+
SOMETHING = 0
13+
IN = 1
14+
THE = 2
15+
WAY = 3
16+
OOOOOOOOOO_OOOOO = 4
17+
18+
19+
class ParentController(Controller):
20+
a: AttrR = AttrR(Int())
21+
b: AttrRW = AttrRW(Int())
22+
23+
24+
class ChildController(SubController):
25+
c: AttrW = AttrW(Int())
26+
27+
@command()
28+
async def d(self):
29+
pass
30+
31+
e: AttrR = AttrR(Bool())
32+
33+
@scan(1)
34+
async def flip_flop(self):
35+
await self.e.set(not self.e.get())
36+
37+
f: AttrRW = AttrRW(Enum(FEnum))
38+
39+
40+
def run():
41+
p4p_options = P4POptions(ioc=P4PIOCOptions(pv_prefix="DEVICE"))
42+
controller = ParentController()
43+
controller.register_sub_controller("Child", ChildController())
44+
fastcs = FastCS(controller, [p4p_options])
45+
fastcs.run()
46+
47+
48+
if __name__ == "__main__":
49+
run()

0 commit comments

Comments
 (0)