11from __future__ import annotations
22
33import asyncio
4- from collections .abc import Callable
5- from typing import Generic
4+ from collections .abc import Awaitable , Callable
5+ from typing import Any , Generic
66
77from fastcs .attribute_io_ref import AttributeIORefT
8- from fastcs .datatypes import (
9- ATTRIBUTE_TYPES ,
10- AttrSetCallback ,
11- AttrUpdateCallback ,
12- DataType ,
13- T ,
14- )
8+ from fastcs .datatypes import ATTRIBUTE_TYPES , DataType , T
159from fastcs .tracer import Tracer
1610
1711ONCE = float ("inf" )
@@ -97,6 +91,10 @@ def __repr__(self):
9791 return f"{ self .__class__ .__name__ } ({ self ._name } , { self ._datatype } )"
9892
9993
94+ AttrUpdateCallback = Callable [["AttrR[T, Any]" ], Awaitable [None ]]
95+ AttrOnSetCallback = Callable [[T ], Awaitable [None ]]
96+
97+
10098class AttrR (Attribute [T , AttributeIORefT ]):
10199 """A read-only ``Attribute``."""
102100
@@ -108,42 +106,66 @@ def __init__(
108106 initial_value : T | None = None ,
109107 description : str | None = None ,
110108 ) -> None :
111- super ().__init__ (
112- datatype , # type: ignore
113- io_ref ,
114- group ,
115- description = description ,
116- )
109+ super ().__init__ (datatype , io_ref , group , description = description )
117110 self ._value : T = (
118111 datatype .initial_value if initial_value is None else initial_value
119112 )
120- self ._on_set_callbacks : list [AttrSetCallback [T ]] | None = None
121- self ._on_update_callbacks : list [AttrUpdateCallback ] | None = None
113+ self ._update_callback : AttrUpdateCallback [T ] | None = None
114+ """Callback to update the value of the attribute from the source"""
115+ self ._on_set_callbacks : list [AttrOnSetCallback [T ]] | None = None
116+ """Callbacks to publish changes to the value of the attribute"""
122117
123118 def get (self ) -> T :
119+ """Get the cached value of the attribute."""
124120 return self ._value
125121
126122 async def set (self , value : T ) -> None :
123+ """Set the value of the attibute
124+
125+ This sets the cached value of the attribute presented in the API. It should
126+ generally only be called from an IO or a controller that is updating the value
127+ from some underlying source.
128+
129+ To request a change to the setpoint of the attribute, use the ``put`` method,
130+ which will attempt to apply the change to the underlying source.
131+
132+ """
127133 self .log_event ("Attribute set" , attribute = self , value = value )
128134
129135 self ._value = self ._datatype .validate (value )
130136
131137 if self ._on_set_callbacks is not None :
132138 await asyncio .gather (* [cb (self ._value ) for cb in self ._on_set_callbacks ])
133139
134- def add_set_callback (self , callback : AttrSetCallback [T ]) -> None :
140+ def add_on_set_callback (self , callback : AttrOnSetCallback [T ]) -> None :
141+ """Add a callback to be called when the attribute is set
142+
143+ The callback will be called with the new value when the attribute is set.
144+
145+ """
135146 if self ._on_set_callbacks is None :
136147 self ._on_set_callbacks = []
137148 self ._on_set_callbacks .append (callback )
138149
139- def add_update_callback (self , callback : AttrUpdateCallback ):
140- if self ._on_update_callbacks is None :
141- self ._on_update_callbacks = []
142- self ._on_update_callbacks .append (callback )
143-
144150 async def update (self ):
145- if self ._on_update_callbacks is not None :
146- await asyncio .gather (* [cb () for cb in self ._on_update_callbacks ])
151+ """Update the attribute value via its IO, if it has one"""
152+ if self ._update_callback is not None :
153+ await self ._update_callback (self )
154+
155+ def set_update_callback (self , callback : AttrUpdateCallback [T ]):
156+ """Set the callback to update the value of the attribute from the source
157+
158+ The callback will be called with the attribute when it needs updating.
159+
160+ """
161+ if self ._update_callback is not None :
162+ raise RuntimeError ("Attribute already has an update callback" )
163+
164+ self ._update_callback = callback
165+
166+
167+ AttrOnPutCallback = Callable [["AttrW[T, Any]" , T ], Awaitable [None ]]
168+ AttrSyncSetpointCallback = Callable [[T ], Awaitable [None ]]
147169
148170
149171class AttrW (Attribute [T , AttributeIORefT ]):
@@ -162,41 +184,83 @@ def __init__(
162184 group ,
163185 description = description ,
164186 )
165- self ._process_callbacks : list [AttrSetCallback [T ]] | None = None
166- self ._write_display_callbacks : list [AttrSetCallback [T ]] | None = None
187+ self ._on_put_callback : AttrOnPutCallback [T ] | None = None
188+ """Callback to action the put of a new value to attribute"""
189+ self ._sync_setpoint_callbacks : list [AttrSyncSetpointCallback [T ]] = []
190+ """Callbacks to publish changes to the setpoint of the attribute"""
191+
192+ async def put (self , setpoint : T , sync_setpoint : bool = False ) -> None :
193+ """Set the setpoint of the attribute
194+
195+ This should be called by clients to the attribute such as transports to apply a
196+ change to the attribute. The ``_on_put_callback`` will be called with this new
197+ setpoint, which may or may not take effect depending on the validity of the new
198+ value. For example, if the attribute has an IO to some device, the value might
199+ be rejected.
200+
201+ To directly change the value of the attribute, for example from an update loop
202+ that has read a new value from some underlying source, call the ``set`` method.
203+
204+ """
205+ setpoint = self ._datatype .validate (setpoint )
206+ if self ._on_put_callback is not None :
207+ await self ._on_put_callback (self , setpoint )
208+
209+ if sync_setpoint :
210+ await self ._call_sync_setpoint_callbacks (setpoint )
211+
212+ async def _call_sync_setpoint_callbacks (self , setpoint : T ) -> None :
213+ if self ._sync_setpoint_callbacks :
214+ await asyncio .gather (
215+ * [cb (setpoint ) for cb in self ._sync_setpoint_callbacks ]
216+ )
167217
168- async def process (self , value : T ) -> None :
169- await self .process_without_display_update (value )
170- await self .update_display_without_process (value )
218+ def set_on_put_callback (self , callback : AttrOnPutCallback [T ]) -> None :
219+ """Set the callback to call when the setpoint is changed
171220
172- async def process_without_display_update (self , value : T ) -> None :
173- value = self ._datatype .validate (value )
174- if self ._process_callbacks :
175- await asyncio .gather (* [cb (value ) for cb in self ._process_callbacks ])
221+ The callback will be called with the attribute and the new setpoint.
176222
177- async def update_display_without_process (self , value : T ) -> None :
178- value = self ._datatype .validate (value )
179- if self ._write_display_callbacks :
180- await asyncio .gather (* [cb (value ) for cb in self ._write_display_callbacks ])
223+ """
224+ if self ._on_put_callback is not None :
225+ raise RuntimeError ("Attribute already has an on put callback" )
181226
182- def add_process_callback (self , callback : AttrSetCallback [T ]) -> None :
183- if self ._process_callbacks is None :
184- self ._process_callbacks = []
185- self ._process_callbacks .append (callback )
227+ self ._on_put_callback = callback
186228
187- def has_process_callback (self ) -> bool :
188- return bool ( self . _process_callbacks )
229+ def add_sync_setpoint_callback (self , callback : AttrSyncSetpointCallback [ T ] ) -> None :
230+ """Add a callback to publish changes to the setpoint of the attribute
189231
190- def add_write_display_callback ( self , callback : AttrSetCallback [ T ]) -> None :
191- if self . _write_display_callbacks is None :
192- self . _write_display_callbacks = []
193- self ._write_display_callbacks .append (callback )
232+ The callback will be called with the new setpoint.
233+
234+ """
235+ self ._sync_setpoint_callbacks .append (callback )
194236
195237
196238class AttrRW (AttrR [T , AttributeIORefT ], AttrW [T , AttributeIORefT ]):
197239 """A read-write ``Attribute``."""
198240
199- async def process (self , value : T ) -> None :
241+ def __init__ (
242+ self ,
243+ datatype : DataType [T ],
244+ io_ref : AttributeIORefT | None = None ,
245+ group : str | None = None ,
246+ initial_value : T | None = None ,
247+ description : str | None = None ,
248+ ):
249+ super ().__init__ (datatype , io_ref , group , initial_value , description )
250+
251+ self ._setpoint_initialised = False
252+
253+ if io_ref is None :
254+ self .set_on_put_callback (self ._internal_put )
255+
256+ async def _internal_put (self , attr : AttrW [T , AttributeIORefT ], value : T ):
257+ """Set value directly when Attribute has no IO"""
258+ assert attr is self
200259 await self .set (value )
201260
202- await super ().process (value ) # type: ignore
261+ async def set (self , value : T ):
262+ await super ().set (value )
263+
264+ if not self ._setpoint_initialised :
265+ await self ._call_sync_setpoint_callbacks (value )
266+ self ._setpoint_initialised = True
0 commit comments