1+ from tango import DeviceProxy , DevState , AttrWriteType , AttrDataFormat , DevString , DevFailed
2+ from tango .server import Device , attribute , command , device_property
3+ import numpy as np
4+ import asyncio
5+ import threading
6+ from dt4acc .core .utils .logger import get_logger
7+ from dt4acc .custom_epics .data .querries import get_unique_power_converters , get_magnets_per_power_converters
8+ from dt4acc .custom_epics .ioc .handlers import update_manager , handle_device_update
9+ from dt4acc .custom_epics .ioc .liasion_translation_manager import build_managers , element_method
10+ from bact_twin_architecture .data_model .identifiers import LatticeElementPropertyID , DevicePropertyID
11+ from bact_twin_architecture .bl .bessyii_yellow_pages import bessyii_yellow_pages
12+
13+ logger = get_logger ()
14+
15+ class MagnetDevice (Device ):
16+ """
17+ Tango device class for controlling magnets in the accelerator.
18+
19+ """
20+
21+ # Device properties
22+ k_value = device_property (
23+ dtype = float ,
24+ default_value = 0.0 ,
25+ doc = "Magnet strength factor (k-value)"
26+ )
27+
28+ name = device_property (
29+ dtype = str ,
30+ default_value = "" ,
31+ doc = "Magnet name"
32+ )
33+
34+ type = device_property (
35+ dtype = str ,
36+ default_value = "unknown" ,
37+ doc = "Magnet type"
38+ )
39+
40+ def init_device (self ):
41+ """Initialize the device."""
42+ try :
43+ print ("\n [DEBUG] Initializing MagnetDevice:" )
44+ print (" - Getting device properties" )
45+ dev_prop = self .get_device_properties ()
46+ print (f" - Device properties: { dev_prop } " )
47+
48+ # Get device name from properties
49+ if not hasattr (dev_prop , 'name' ) or not dev_prop .name :
50+ print (" - ERROR: Device name property not set" )
51+ # Try to get name from device name
52+ device_name = self .get_name ()
53+ print (f" - Device name from get_name(): { device_name } " )
54+ if device_name :
55+ # Extract name from device name (format: tango_server/test/MagnetDevice_HS1MD4R)
56+ try :
57+ self .name = device_name .split ('_' )[- 1 ]
58+ print (f" - Extracted name from device name: { self .name } " )
59+ except :
60+ print (" - Could not extract name from device name" )
61+ raise DevFailed ("Device name property not set" )
62+ else :
63+ raise DevFailed ("Device name property not set" )
64+
65+ print (f" - Device name: { self .name } " )
66+
67+ # Initialize attributes
68+ try :
69+ print (" - Initializing attributes" )
70+ self ._init_attributes ()
71+ print (" - Attributes initialized successfully" )
72+ except Exception as e :
73+ print (f" - ERROR: Failed to initialize attributes: { str (e )} " )
74+ raise
75+
76+ # Initialize event loop
77+ try :
78+ self ._loop = asyncio .new_event_loop ()
79+ asyncio .set_event_loop (self ._loop )
80+ # Start event loop in a separate thread
81+ self ._loop_thread = threading .Thread (target = self ._run_event_loop , daemon = True )
82+ self ._loop_thread .start ()
83+ print (" - Event loop initialized successfully" )
84+ except Exception as e :
85+ print (f" - ERROR: Failed to initialize event loop: { str (e )} " )
86+ raise
87+
88+ print (" - Device initialization completed successfully" )
89+
90+ except Exception as e :
91+ print (f"\n [ERROR] Device initialization failed:" )
92+ print (f" - Error: { str (e )} " )
93+ raise
94+
95+ def _run_event_loop (self ):
96+ """Run the event loop in a separate thread."""
97+ asyncio .set_event_loop (self ._loop )
98+ self ._loop .run_forever ()
99+
100+ def _init_attributes (self ):
101+ """Initialize device attributes with default values."""
102+ try :
103+ print (" - Initializing device attributes" )
104+
105+ # Get initial values from update manager
106+ try :
107+ val = update_manager .peek_engine (
108+ LatticeElementPropertyID (element_name = self .name , property = "main_strength" )
109+ )
110+ print (f" * Initial magnetic strength value: { val } " )
111+ except Exception as e :
112+ logger .warning (f"Could not get initial values from update manager: { str (e )} " )
113+ val = 0.0
114+
115+ # Get associated power converter from EPICS query
116+ try :
117+ print (f" * Looking up power converter for magnet { self .name } " )
118+ power_converters = get_unique_power_converters ()
119+ print (f" * Found { len (power_converters )} power converters" )
120+
121+ # Debug print all power converters and their magnets
122+ for pc in power_converters :
123+ magnets = get_magnets_per_power_converters (pc )
124+ print (f" * Power converter { pc } has magnets: { [m ['name' ] for m in magnets ]} " )
125+
126+ # Find power converter for this magnet
127+ self ._power_converter = None
128+ for pc in power_converters :
129+ magnets = get_magnets_per_power_converters (pc )
130+ if any (m ['name' ] == self .name for m in magnets ):
131+ self ._power_converter = pc
132+ print (f" * Found power converter { pc } for magnet { self .name } " )
133+ break
134+
135+ if not self ._power_converter :
136+ print (f" * WARNING: No power converter found for magnet { self .name } " )
137+ logger .warning (f"No power converter found for magnet { self .name } " )
138+ else :
139+ logger .info (f"Found associated power converter: { self ._power_converter } " )
140+ except Exception as e :
141+ print (f" * ERROR: Failed to get power converter: { str (e )} " )
142+ logger .warning (f"Could not get power converter from EPICS query: { str (e )} " )
143+ self ._power_converter = None
144+
145+ # Initialize attributes with default values
146+ self ._magnetic_strength = self .k_value or 0.0 # as like Cm:set in EPICS
147+ self ._magnetic_strength_readback = val # like Cm:rdbk in EPICS
148+ self ._current = 0.0 # im:I in EPICS
149+ self ._power_supply_current = 0.0
150+ self ._x_position = 0.0 # x:set like in EPICS
151+ self ._y_position = 0.0 # y:set like in EPICS
152+ self ._cm_set = val
153+
154+ print (" * All attributes initialized successfully" )
155+
156+ except Exception as e :
157+ print (f" * ERROR: Failed to initialize attributes: { str (e )} " )
158+ raise
159+
160+ def _run_async_update (self , update_func ):
161+ """Helper method to run async updates in the event loop."""
162+ try :
163+ future = asyncio .run_coroutine_threadsafe (update_func , self ._loop )
164+ future .result (timeout = 10.0 ) # Increased timeout to 10 seconds
165+ except asyncio .TimeoutError :
166+ logger .error ("Async update timed out after 10 seconds" )
167+ raise DevFailed ("Update operation timed out" )
168+ except Exception as e :
169+ logger .error (f"Error in async update: { str (e )} " )
170+ raise
171+
172+ # Magnetic field control attributes
173+ @attribute (dtype = float , access = AttrWriteType .READ_WRITE )
174+ def magnetic_strength (self ):
175+ """Get magnetic strength (Cm:set in EPICS)."""
176+ return self ._magnetic_strength
177+
178+ @magnetic_strength .write
179+ def magnetic_strength (self , value ):
180+ """Set magnetic strength (Cm:set in EPICS)."""
181+ try :
182+ self ._magnetic_strength = float (value )
183+
184+ # Get associated power converter
185+ if not hasattr (self , '_power_converter' ) or self ._power_converter is None :
186+ print (f"Looking up power converter for magnet { self .name } " )
187+ power_converters = get_unique_power_converters ()
188+ print (f"Found { len (power_converters )} power converters" )
189+
190+ # Find power converter for this magnet
191+ self ._power_converter = None
192+ for pc in power_converters :
193+ magnets = get_magnets_per_power_converters (pc )
194+ if any (m ['name' ] == self .name for m in magnets ):
195+ self ._power_converter = pc
196+ print (f"Found power converter { pc } for magnet { self .name } " )
197+ break
198+
199+ if not self ._power_converter :
200+ raise DevFailed (f"No power converter found for magnet { self .name } " )
201+
202+ print (f"Using power converter { self ._power_converter } for magnet { self .name } " )
203+ print (f"Current magnetic strength: { self ._magnetic_strength } " )
204+ print (f"Previous magnetic strength: { self ._magnetic_strength_readback } " )
205+
206+ try :
207+ # Update only the power converter's set_current
208+ # The LiaisonManager will handle mapping this to the appropriate lattice property
209+ print (f"Updating power converter { self ._power_converter } set_current to { value } " )
210+ self ._run_async_update (handle_device_update (self ._power_converter , "set_current" , value ))
211+
212+ # Update readback to match EPICS behavior
213+ self ._magnetic_strength_readback = value
214+ logger .info (f"Updated magnetic strength to { value } using power converter { self ._power_converter } " )
215+
216+ except Exception as e :
217+ if "array must not contain infs or NaNs" in str (e ):
218+ logger .warning ("Twiss calculation error - continuing with update" )
219+ logger .warning (f"Error occurred with magnetic strength value: { value } " )
220+ # Still update the readback since the value was set
221+ self ._magnetic_strength_readback = value
222+ else :
223+ raise
224+
225+ except Exception as e :
226+ logger .error (f"Error updating magnetic strength: { str (e )} " )
227+ raise
228+
229+ @attribute (dtype = float )
230+ def magnetic_strength_readback (self ):
231+ """Get magnetic strength readback (Cm:rdbk in EPICS)."""
232+ return self ._magnetic_strength_readback
233+
234+ @attribute (dtype = float , access = AttrWriteType .READ_WRITE )
235+ def current (self ):
236+ """Get current (im:I in EPICS)."""
237+ return self ._current
238+
239+ @current .write
240+ def current (self , value ):
241+ """Set current (im:I in EPICS)."""
242+ try :
243+ self ._current = float (value )
244+ # Use EPICS update handler with set_current property
245+ if self ._power_converter :
246+ self ._run_async_update (handle_device_update (self ._power_converter , "set_current" , value ))
247+ logger .info (f"Updated current to { value } " )
248+ except Exception as e :
249+ logger .error (f"Error updating current: { str (e )} " )
250+ raise
251+
252+ @attribute (dtype = float )
253+ def power_supply_current (self ):
254+ """Get power supply current."""
255+ return self ._power_supply_current
256+
257+ @attribute (dtype = float , access = AttrWriteType .READ_WRITE )
258+ def x_position (self ):
259+ """Get x position (x:set in EPICS)."""
260+ return self ._x_position
261+
262+ @x_position .write
263+ def x_position (self , value ):
264+ """Set x position (x:set in EPICS)."""
265+ try :
266+ self ._x_position = float (value )
267+ # Use EPICS update handler with x_kick property
268+ self ._run_async_update (handle_device_update (self .name , "x_kick" , value ))
269+ logger .info (f"Updated x position to { value } " )
270+ except Exception as e :
271+ logger .error (f"Error updating x position: { str (e )} " )
272+ raise
273+
274+ @attribute (dtype = float , access = AttrWriteType .READ_WRITE )
275+ def y_position (self ):
276+ """Get y position (y:set in EPICS)."""
277+ return self ._y_position
278+
279+ @y_position .write
280+ def y_position (self , value ):
281+ """Set y position (y:set in EPICS)."""
282+ try :
283+ self ._y_position = float (value )
284+ # Use EPICS update handler with y_kick property
285+ self ._run_async_update (handle_device_update (self .name , "y_kick" , value ))
286+ logger .info (f"Updated y position to { value } " )
287+ except Exception as e :
288+ logger .error (f"Error updating y position: { str (e )} " )
289+ raise
290+
291+ @attribute (dtype = float )
292+ def cm_set (self ):
293+ """Get Cm set value."""
294+ return self ._cm_set
295+
296+ # Commands
297+ @command (dtype_in = None , doc_in = "Reset the magnet to its default state" )
298+ def reset (self ):
299+ """Reset the magnet to its default state."""
300+ try :
301+ self ._magnetic_strength = self .k_value or 0.0
302+ self ._magnetic_strength_readback = self .k_value or 0.0
303+ self ._current = 0.0
304+ self ._power_supply_current = 0.0
305+ self ._x_position = 0.0
306+ self ._y_position = 0.0
307+ self .set_state (DevState .OFF )
308+ logger .info (f"Magnet { self .name } : Reset to default state" )
309+ except Exception as e :
310+ logger .error (f"Error resetting magnet: { str (e )} " )
311+ raise
312+
313+ @command (dtype_in = None , doc_in = "Calibrate the magnet" )
314+ def calibrate (self ):
315+ """Calibrate the magnet."""
316+ try :
317+ # Here you would implement the actual calibration logic
318+ logger .info (f"Magnet { self .name } : Calibration started" )
319+ # Simulate calibration
320+ self ._magnetic_strength_readback = self ._magnetic_strength
321+ logger .info (f"Magnet { self .name } : Calibration completed" )
322+ except Exception as e :
323+ logger .error (f"Error calibrating magnet: { str (e )} " )
324+ raise
0 commit comments