ophyd-async 0.1.0__py3-none-any.whl → 0.3.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (94) hide show
  1. ophyd_async/__init__.py +1 -4
  2. ophyd_async/_version.py +2 -2
  3. ophyd_async/core/__init__.py +91 -19
  4. ophyd_async/core/_providers.py +68 -0
  5. ophyd_async/core/async_status.py +90 -42
  6. ophyd_async/core/detector.py +341 -0
  7. ophyd_async/core/device.py +226 -0
  8. ophyd_async/core/device_save_loader.py +286 -0
  9. ophyd_async/core/flyer.py +85 -0
  10. ophyd_async/core/mock_signal_backend.py +82 -0
  11. ophyd_async/core/mock_signal_utils.py +145 -0
  12. ophyd_async/core/{_device/_signal/signal.py → signal.py} +249 -61
  13. ophyd_async/core/{_device/_backend/signal_backend.py → signal_backend.py} +12 -5
  14. ophyd_async/core/{_device/_backend/sim_signal_backend.py → soft_signal_backend.py} +54 -48
  15. ophyd_async/core/standard_readable.py +261 -0
  16. ophyd_async/core/utils.py +127 -30
  17. ophyd_async/epics/_backend/_aioca.py +62 -43
  18. ophyd_async/epics/_backend/_p4p.py +100 -52
  19. ophyd_async/epics/_backend/common.py +25 -0
  20. ophyd_async/epics/areadetector/__init__.py +16 -15
  21. ophyd_async/epics/areadetector/aravis.py +63 -0
  22. ophyd_async/epics/areadetector/controllers/__init__.py +5 -0
  23. ophyd_async/epics/areadetector/controllers/ad_sim_controller.py +52 -0
  24. ophyd_async/epics/areadetector/controllers/aravis_controller.py +78 -0
  25. ophyd_async/epics/areadetector/controllers/kinetix_controller.py +49 -0
  26. ophyd_async/epics/areadetector/controllers/pilatus_controller.py +61 -0
  27. ophyd_async/epics/areadetector/controllers/vimba_controller.py +66 -0
  28. ophyd_async/epics/areadetector/drivers/__init__.py +21 -0
  29. ophyd_async/epics/areadetector/drivers/ad_base.py +107 -0
  30. ophyd_async/epics/areadetector/drivers/aravis_driver.py +38 -0
  31. ophyd_async/epics/areadetector/drivers/kinetix_driver.py +27 -0
  32. ophyd_async/epics/areadetector/drivers/pilatus_driver.py +21 -0
  33. ophyd_async/epics/areadetector/drivers/vimba_driver.py +63 -0
  34. ophyd_async/epics/areadetector/kinetix.py +46 -0
  35. ophyd_async/epics/areadetector/pilatus.py +45 -0
  36. ophyd_async/epics/areadetector/single_trigger_det.py +18 -10
  37. ophyd_async/epics/areadetector/utils.py +91 -13
  38. ophyd_async/epics/areadetector/vimba.py +43 -0
  39. ophyd_async/epics/areadetector/writers/__init__.py +5 -0
  40. ophyd_async/epics/areadetector/writers/_hdfdataset.py +10 -0
  41. ophyd_async/epics/areadetector/writers/_hdffile.py +54 -0
  42. ophyd_async/epics/areadetector/writers/hdf_writer.py +142 -0
  43. ophyd_async/epics/areadetector/writers/nd_file_hdf.py +40 -0
  44. ophyd_async/epics/areadetector/writers/nd_plugin.py +38 -0
  45. ophyd_async/epics/demo/__init__.py +78 -51
  46. ophyd_async/epics/demo/demo_ad_sim_detector.py +35 -0
  47. ophyd_async/epics/motion/motor.py +67 -52
  48. ophyd_async/epics/pvi/__init__.py +3 -0
  49. ophyd_async/epics/pvi/pvi.py +318 -0
  50. ophyd_async/epics/signal/__init__.py +8 -3
  51. ophyd_async/epics/signal/signal.py +27 -10
  52. ophyd_async/log.py +130 -0
  53. ophyd_async/panda/__init__.py +24 -7
  54. ophyd_async/panda/_common_blocks.py +49 -0
  55. ophyd_async/panda/_hdf_panda.py +48 -0
  56. ophyd_async/panda/_panda_controller.py +37 -0
  57. ophyd_async/panda/_table.py +158 -0
  58. ophyd_async/panda/_trigger.py +39 -0
  59. ophyd_async/panda/_utils.py +15 -0
  60. ophyd_async/panda/writers/__init__.py +3 -0
  61. ophyd_async/panda/writers/_hdf_writer.py +220 -0
  62. ophyd_async/panda/writers/_panda_hdf_file.py +58 -0
  63. ophyd_async/plan_stubs/__init__.py +13 -0
  64. ophyd_async/plan_stubs/ensure_connected.py +22 -0
  65. ophyd_async/plan_stubs/fly.py +149 -0
  66. ophyd_async/protocols.py +126 -0
  67. ophyd_async/sim/__init__.py +11 -0
  68. ophyd_async/sim/demo/__init__.py +3 -0
  69. ophyd_async/sim/demo/sim_motor.py +103 -0
  70. ophyd_async/sim/pattern_generator.py +318 -0
  71. ophyd_async/sim/sim_pattern_detector_control.py +55 -0
  72. ophyd_async/sim/sim_pattern_detector_writer.py +34 -0
  73. ophyd_async/sim/sim_pattern_generator.py +37 -0
  74. {ophyd_async-0.1.0.dist-info → ophyd_async-0.3.0.dist-info}/METADATA +35 -67
  75. ophyd_async-0.3.0.dist-info/RECORD +86 -0
  76. {ophyd_async-0.1.0.dist-info → ophyd_async-0.3.0.dist-info}/WHEEL +1 -1
  77. ophyd_async/core/_device/__init__.py +0 -0
  78. ophyd_async/core/_device/_backend/__init__.py +0 -0
  79. ophyd_async/core/_device/_signal/__init__.py +0 -0
  80. ophyd_async/core/_device/device.py +0 -60
  81. ophyd_async/core/_device/device_collector.py +0 -121
  82. ophyd_async/core/_device/device_vector.py +0 -14
  83. ophyd_async/core/_device/standard_readable.py +0 -72
  84. ophyd_async/epics/areadetector/ad_driver.py +0 -18
  85. ophyd_async/epics/areadetector/directory_provider.py +0 -18
  86. ophyd_async/epics/areadetector/hdf_streamer_det.py +0 -167
  87. ophyd_async/epics/areadetector/nd_file_hdf.py +0 -22
  88. ophyd_async/epics/areadetector/nd_plugin.py +0 -13
  89. ophyd_async/epics/signal/pvi_get.py +0 -22
  90. ophyd_async/panda/panda.py +0 -332
  91. ophyd_async-0.1.0.dist-info/RECORD +0 -45
  92. {ophyd_async-0.1.0.dist-info → ophyd_async-0.3.0.dist-info}/LICENSE +0 -0
  93. {ophyd_async-0.1.0.dist-info → ophyd_async-0.3.0.dist-info}/entry_points.txt +0 -0
  94. {ophyd_async-0.1.0.dist-info → ophyd_async-0.3.0.dist-info}/top_level.txt +0 -0
@@ -2,24 +2,37 @@ from __future__ import annotations
2
2
 
3
3
  import asyncio
4
4
  import functools
5
- from typing import AsyncGenerator, Callable, Dict, Generic, Optional, Union
5
+ from typing import (
6
+ Any,
7
+ AsyncGenerator,
8
+ Callable,
9
+ Dict,
10
+ Generic,
11
+ Mapping,
12
+ Optional,
13
+ Tuple,
14
+ Type,
15
+ Union,
16
+ )
6
17
 
7
18
  from bluesky.protocols import (
8
- Descriptor,
19
+ DataKey,
20
+ Locatable,
21
+ Location,
9
22
  Movable,
10
- Readable,
11
23
  Reading,
12
- Stageable,
24
+ Status,
13
25
  Subscribable,
14
26
  )
15
27
 
16
- from ...async_status import AsyncStatus
17
- from ...utils import DEFAULT_TIMEOUT, Callback, ReadingValueCallback, T
18
- from .._backend.signal_backend import SignalBackend
19
- from .._backend.sim_signal_backend import SimSignalBackend
20
- from ..device import Device
28
+ from ophyd_async.core.mock_signal_backend import MockSignalBackend
29
+ from ophyd_async.protocols import AsyncConfigurable, AsyncReadable, AsyncStageable
21
30
 
22
- _sim_backends: Dict[Signal, SimSignalBackend] = {}
31
+ from .async_status import AsyncStatus
32
+ from .device import Device
33
+ from .signal_backend import SignalBackend
34
+ from .soft_signal_backend import SoftSignalBackend
35
+ from .utils import DEFAULT_TIMEOUT, CalculatableTimeout, CalculateTimeout, Callback, T
23
36
 
24
37
 
25
38
  def _add_timeout(func):
@@ -43,34 +56,30 @@ class Signal(Device, Generic[T]):
43
56
  """A Device with the concept of a value, with R, RW, W and X flavours"""
44
57
 
45
58
  def __init__(
46
- self, backend: SignalBackend[T], timeout: Optional[float] = DEFAULT_TIMEOUT
59
+ self,
60
+ backend: SignalBackend[T],
61
+ timeout: Optional[float] = DEFAULT_TIMEOUT,
62
+ name: str = "",
47
63
  ) -> None:
48
- self._name = ""
49
64
  self._timeout = timeout
50
- self._init_backend = self._backend = backend
51
-
52
- @property
53
- def name(self) -> str:
54
- return self._name
55
-
56
- def set_name(self, name: str = ""):
57
- self._name = name
58
-
59
- async def connect(self, sim=False):
60
- if sim:
61
- self._backend = SimSignalBackend(
62
- datatype=self._init_backend.datatype, source=self._init_backend.source
65
+ self._initial_backend = self._backend = backend
66
+ super().__init__(name)
67
+
68
+ async def connect(
69
+ self, mock=False, timeout=DEFAULT_TIMEOUT, force_reconnect: bool = False
70
+ ):
71
+ if mock and not isinstance(self._backend, MockSignalBackend):
72
+ # Using a soft backend, look to the initial value
73
+ self._backend = MockSignalBackend(
74
+ initial_backend=self._initial_backend,
63
75
  )
64
- _sim_backends[self] = self._backend
65
- else:
66
- self._backend = self._init_backend
67
- _sim_backends.pop(self, None)
68
- await self._backend.connect()
76
+ self.log.debug(f"Connecting to {self.source}")
77
+ await self._backend.connect(timeout=timeout)
69
78
 
70
79
  @property
71
80
  def source(self) -> str:
72
81
  """Like ca://PV_PREFIX:SIGNAL, or "" if not set"""
73
- return self._backend.source
82
+ return self._backend.source(self.name)
74
83
 
75
84
  __lt__ = __le__ = __eq__ = __ge__ = __gt__ = __ne__ = _fail
76
85
 
@@ -89,10 +98,12 @@ class _SignalCache(Generic[T]):
89
98
  self._value: Optional[T] = None
90
99
 
91
100
  self.backend = backend
101
+ signal.log.debug(f"Making subscription on source {signal.source}")
92
102
  backend.set_callback(self._callback)
93
103
 
94
104
  def close(self):
95
105
  self.backend.set_callback(None)
106
+ self._signal.log.debug(f"Closing subscription on source {self._signal.source}")
96
107
 
97
108
  async def get_reading(self) -> Reading:
98
109
  await self._valid.wait()
@@ -105,6 +116,10 @@ class _SignalCache(Generic[T]):
105
116
  return self._value
106
117
 
107
118
  def _callback(self, reading: Reading, value: T):
119
+ self._signal.log.debug(
120
+ f"Updated subscription: reading of source {self._signal.source} changed"
121
+ f"from {self._reading} to {reading}"
122
+ )
108
123
  self._reading = reading
109
124
  self._value = value
110
125
  self._valid.set()
@@ -131,7 +146,7 @@ class _SignalCache(Generic[T]):
131
146
  return self._staged or bool(self._listeners)
132
147
 
133
148
 
134
- class SignalR(Signal[T], Readable, Stageable, Subscribable):
149
+ class SignalR(Signal[T], AsyncReadable, AsyncStageable, Subscribable):
135
150
  """Signal that can be read from and monitored"""
136
151
 
137
152
  _cache: Optional[_SignalCache] = None
@@ -164,14 +179,16 @@ class SignalR(Signal[T], Readable, Stageable, Subscribable):
164
179
  return {self.name: await self._backend_or_cache(cached).get_reading()}
165
180
 
166
181
  @_add_timeout
167
- async def describe(self) -> Dict[str, Descriptor]:
182
+ async def describe(self) -> Dict[str, DataKey]:
168
183
  """Return a single item dict with the descriptor in it"""
169
- return {self.name: await self._backend.get_descriptor()}
184
+ return {self.name: await self._backend.get_datakey(self.source)}
170
185
 
171
186
  @_add_timeout
172
187
  async def get_value(self, cached: Optional[bool] = None) -> T:
173
188
  """The current value"""
174
- return await self._backend_or_cache(cached).get_value()
189
+ value = await self._backend_or_cache(cached).get_value()
190
+ self.log.debug(f"get_value() on source {self.source} returned {value}")
191
+ return value
175
192
 
176
193
  def subscribe_value(self, function: Callback[T]):
177
194
  """Subscribe to updates in value of a device"""
@@ -199,44 +216,187 @@ class SignalR(Signal[T], Readable, Stageable, Subscribable):
199
216
  class SignalW(Signal[T], Movable):
200
217
  """Signal that can be set"""
201
218
 
202
- def set(self, value: T, wait=True, timeout=None) -> AsyncStatus:
219
+ def set(
220
+ self, value: T, wait=True, timeout: CalculatableTimeout = CalculateTimeout
221
+ ) -> AsyncStatus:
203
222
  """Set the value and return a status saying when it's done"""
204
- coro = self._backend.put(value, wait=wait, timeout=timeout or self._timeout)
205
- return AsyncStatus(coro)
223
+ if timeout is CalculateTimeout:
224
+ timeout = self._timeout
225
+
226
+ async def do_set():
227
+ self.log.debug(f"Putting value {value} to backend at source {self.source}")
228
+ await self._backend.put(value, wait=wait, timeout=timeout)
229
+ self.log.debug(
230
+ f"Successfully put value {value} to backend at source {self.source}"
231
+ )
206
232
 
233
+ return AsyncStatus(do_set())
207
234
 
208
- class SignalRW(SignalR[T], SignalW[T]):
235
+
236
+ class SignalRW(SignalR[T], SignalW[T], Locatable):
209
237
  """Signal that can be both read and set"""
210
238
 
239
+ async def locate(self) -> Location:
240
+ location: Location = {
241
+ "setpoint": await self._backend.get_setpoint(),
242
+ "readback": await self.get_value(),
243
+ }
244
+ return location
245
+
211
246
 
212
247
  class SignalX(Signal):
213
248
  """Signal that puts the default value"""
214
249
 
215
- async def execute(self, wait=True, timeout=None):
216
- """Execute the action and return a status saying when it's done"""
217
- await self._backend.put(None, wait=wait, timeout=timeout or self._timeout)
250
+ def trigger(
251
+ self, wait=True, timeout: CalculatableTimeout = CalculateTimeout
252
+ ) -> AsyncStatus:
253
+ """Trigger the action and return a status saying when it's done"""
254
+ if timeout is CalculateTimeout:
255
+ timeout = self._timeout
256
+ coro = self._backend.put(None, wait=wait, timeout=timeout)
257
+ return AsyncStatus(coro)
258
+
218
259
 
260
+ def soft_signal_rw(
261
+ datatype: Optional[Type[T]] = None,
262
+ initial_value: Optional[T] = None,
263
+ name: str = "",
264
+ ) -> SignalRW[T]:
265
+ """Creates a read-writable Signal with a SoftSignalBackend"""
266
+ signal = SignalRW(SoftSignalBackend(datatype, initial_value), name=name)
267
+ return signal
268
+
269
+
270
+ def soft_signal_r_and_setter(
271
+ datatype: Optional[Type[T]] = None,
272
+ initial_value: Optional[T] = None,
273
+ name: str = "",
274
+ ) -> Tuple[SignalR[T], Callable[[T], None]]:
275
+ """Returns a tuple of a read-only Signal and a callable through
276
+ which the signal can be internally modified within the device. Use
277
+ soft_signal_rw if you want a device that is externally modifiable
278
+ """
279
+ backend = SoftSignalBackend(datatype, initial_value)
280
+ signal = SignalR(backend, name=name)
219
281
 
220
- def set_sim_value(signal: Signal[T], value: T):
221
- """Set the value of a signal that is in sim mode."""
222
- _sim_backends[signal]._set_value(value)
282
+ return (signal, backend.set_value)
223
283
 
224
284
 
225
- def set_sim_put_proceeds(signal: Signal[T], proceeds: bool):
226
- """Allow or block a put with wait=True from proceeding"""
227
- event = _sim_backends[signal].put_proceeds
228
- if proceeds:
229
- event.set()
230
- else:
231
- event.clear()
285
+ def _generate_assert_error_msg(
286
+ name: str, expected_result: str, actuall_result: str
287
+ ) -> str:
288
+ WARNING = "\033[93m"
289
+ FAIL = "\033[91m"
290
+ ENDC = "\033[0m"
291
+ return (
292
+ f"Expected {WARNING}{name}{ENDC} to produce"
293
+ + f"\n{FAIL}{actuall_result}{ENDC}"
294
+ + f"\nbut actually got \n{FAIL}{expected_result}{ENDC}"
295
+ )
296
+
297
+
298
+ async def assert_value(signal: SignalR[T], value: Any) -> None:
299
+ """Assert a signal's value and compare it an expected signal.
300
+
301
+ Parameters
302
+ ----------
303
+ signal:
304
+ signal with get_value.
305
+ value:
306
+ The expected value from the signal.
307
+
308
+ Notes
309
+ -----
310
+ Example usage::
311
+ await assert_value(signal, value)
312
+
313
+ """
314
+ actual_value = await signal.get_value()
315
+ assert actual_value == value, _generate_assert_error_msg(
316
+ signal.name, value, actual_value
317
+ )
318
+
319
+
320
+ async def assert_reading(
321
+ readable: AsyncReadable, expected_reading: Mapping[str, Reading]
322
+ ) -> None:
323
+ """Assert readings from readable.
324
+
325
+ Parameters
326
+ ----------
327
+ readable:
328
+ Callable with readable.read function that generate readings.
232
329
 
330
+ reading:
331
+ The expected readings from the readable.
233
332
 
234
- def set_sim_callback(signal: Signal[T], callback: ReadingValueCallback[T]) -> None:
235
- """Monitor the value of a signal that is in sim mode"""
236
- return _sim_backends[signal].set_callback(callback)
333
+ Notes
334
+ -----
335
+ Example usage::
336
+ await assert_reading(readable, reading)
337
+
338
+ """
339
+ actual_reading = await readable.read()
340
+ assert expected_reading == actual_reading, _generate_assert_error_msg(
341
+ readable.name, expected_reading, actual_reading
342
+ )
343
+
344
+
345
+ async def assert_configuration(
346
+ configurable: AsyncConfigurable,
347
+ configuration: Mapping[str, Reading],
348
+ ) -> None:
349
+ """Assert readings from Configurable.
350
+
351
+ Parameters
352
+ ----------
353
+ configurable:
354
+ Configurable with Configurable.read function that generate readings.
355
+
356
+ configuration:
357
+ The expected readings from configurable.
358
+
359
+ Notes
360
+ -----
361
+ Example usage::
362
+ await assert_configuration(configurable configuration)
363
+
364
+ """
365
+ actual_configurable = await configurable.read_configuration()
366
+ assert configuration == actual_configurable, _generate_assert_error_msg(
367
+ configurable.name, configuration, actual_configurable
368
+ )
237
369
 
238
370
 
239
- async def observe_value(signal: SignalR[T]) -> AsyncGenerator[T, None]:
371
+ def assert_emitted(docs: Mapping[str, list[dict]], **numbers: int):
372
+ """Assert emitted document generated by running a Bluesky plan
373
+
374
+ Parameters
375
+ ----------
376
+ Doc:
377
+ A dictionary
378
+
379
+ numbers:
380
+ expected emission in kwarg from
381
+
382
+ Notes
383
+ -----
384
+ Example usage::
385
+ assert_emitted(docs, start=1, descriptor=1,
386
+ resource=1, datum=1, event=1, stop=1)
387
+ """
388
+ assert list(docs) == list(numbers), _generate_assert_error_msg(
389
+ "documents", list(numbers), list(docs)
390
+ )
391
+ actual_numbers = {name: len(d) for name, d in docs.items()}
392
+ assert actual_numbers == numbers, _generate_assert_error_msg(
393
+ "emitted", numbers, actual_numbers
394
+ )
395
+
396
+
397
+ async def observe_value(
398
+ signal: SignalR[T], timeout: float | None = None, done_status: Status | None = None
399
+ ) -> AsyncGenerator[T, None]:
240
400
  """Subscribe to the value of a signal so it can be iterated from.
241
401
 
242
402
  Parameters
@@ -244,6 +404,12 @@ async def observe_value(signal: SignalR[T]) -> AsyncGenerator[T, None]:
244
404
  signal:
245
405
  Call subscribe_value on this at the start, and clear_sub on it at the
246
406
  end
407
+ timeout:
408
+ If given, how long to wait for each updated value in seconds. If an update
409
+ is not produced in this time then raise asyncio.TimeoutError
410
+ done_status:
411
+ If this status is complete, stop observing and make the iterator return.
412
+ If it raises an exception then this exception will be raised by the iterator.
247
413
 
248
414
  Notes
249
415
  -----
@@ -252,18 +418,36 @@ async def observe_value(signal: SignalR[T]) -> AsyncGenerator[T, None]:
252
418
  async for value in observe_value(sig):
253
419
  do_something_with(value)
254
420
  """
255
- q: asyncio.Queue[T] = asyncio.Queue()
421
+
422
+ q: asyncio.Queue[T | Status] = asyncio.Queue()
423
+ if timeout is None:
424
+ get_value = q.get
425
+ else:
426
+
427
+ async def get_value():
428
+ return await asyncio.wait_for(q.get(), timeout)
429
+
430
+ if done_status is not None:
431
+ done_status.add_callback(q.put_nowait)
432
+
256
433
  signal.subscribe_value(q.put_nowait)
257
434
  try:
258
435
  while True:
259
- yield await q.get()
436
+ item = await get_value()
437
+ if done_status and item is done_status:
438
+ if exc := done_status.exception():
439
+ raise exc
440
+ else:
441
+ break
442
+ else:
443
+ yield item
260
444
  finally:
261
445
  signal.clear_sub(q.put_nowait)
262
446
 
263
447
 
264
448
  class _ValueChecker(Generic[T]):
265
449
  def __init__(self, matcher: Callable[[T], bool], matcher_name: str):
266
- self._last_value: Optional[T]
450
+ self._last_value: Optional[T] = None
267
451
  self._matcher = matcher
268
452
  self._matcher_name = matcher_name
269
453
 
@@ -273,7 +457,7 @@ class _ValueChecker(Generic[T]):
273
457
  if self._matcher(value):
274
458
  return
275
459
 
276
- async def wait_for_value(self, signal: SignalR[T], timeout: float):
460
+ async def wait_for_value(self, signal: SignalR[T], timeout: Optional[float]):
277
461
  try:
278
462
  await asyncio.wait_for(self._wait_for_value(signal), timeout)
279
463
  except asyncio.TimeoutError as e:
@@ -284,7 +468,7 @@ class _ValueChecker(Generic[T]):
284
468
 
285
469
 
286
470
  async def wait_for_value(
287
- signal: SignalR[T], match: Union[T, Callable[[T], bool]], timeout: float
471
+ signal: SignalR[T], match: Union[T, Callable[[T], bool]], timeout: Optional[float]
288
472
  ):
289
473
  """Wait for a signal to have a matching value.
290
474
 
@@ -330,6 +514,10 @@ async def set_and_wait_for_value(
330
514
  - Read the same Signal to check the operation has started
331
515
  - Return the Status so calling code can wait for operation to complete
332
516
 
517
+ This function sets a signal to a specified value, optionally with or without a
518
+ ca/pv put callback, and waits for the readback value of the signal to match the
519
+ value it was set to.
520
+
333
521
  Parameters
334
522
  ----------
335
523
  signal:
@@ -1,9 +1,9 @@
1
1
  from abc import abstractmethod
2
2
  from typing import Generic, Optional, Type
3
3
 
4
- from bluesky.protocols import Descriptor, Reading
4
+ from bluesky.protocols import DataKey, Reading
5
5
 
6
- from ...utils import ReadingValueCallback, T
6
+ from .utils import DEFAULT_TIMEOUT, ReadingValueCallback, T
7
7
 
8
8
 
9
9
  class SignalBackend(Generic[T]):
@@ -13,10 +13,13 @@ class SignalBackend(Generic[T]):
13
13
  datatype: Optional[Type[T]] = None
14
14
 
15
15
  #: Like ca://PV_PREFIX:SIGNAL
16
- source: str = ""
16
+ @abstractmethod
17
+ def source(self, name: str) -> str:
18
+ """Return source of signal. Signals may pass a name to the backend, which can be
19
+ used or discarded."""
17
20
 
18
21
  @abstractmethod
19
- async def connect(self):
22
+ async def connect(self, timeout: float = DEFAULT_TIMEOUT):
20
23
  """Connect to underlying hardware"""
21
24
 
22
25
  @abstractmethod
@@ -24,7 +27,7 @@ class SignalBackend(Generic[T]):
24
27
  """Put a value to the PV, if wait then wait for completion for up to timeout"""
25
28
 
26
29
  @abstractmethod
27
- async def get_descriptor(self) -> Descriptor:
30
+ async def get_datakey(self, source: str) -> DataKey:
28
31
  """Metadata like source, dtype, shape, precision, units"""
29
32
 
30
33
  @abstractmethod
@@ -35,6 +38,10 @@ class SignalBackend(Generic[T]):
35
38
  async def get_value(self) -> T:
36
39
  """The current value"""
37
40
 
41
+ @abstractmethod
42
+ async def get_setpoint(self) -> T:
43
+ """The point that a signal was requested to move to."""
44
+
38
45
  @abstractmethod
39
46
  def set_callback(self, callback: Optional[ReadingValueCallback[T]]) -> None:
40
47
  """Observe changes to the current value, timestamp and severity"""
@@ -1,18 +1,17 @@
1
1
  from __future__ import annotations
2
2
 
3
- import asyncio
4
3
  import inspect
5
- import re
6
4
  import time
7
5
  from collections import abc
8
6
  from dataclasses import dataclass
9
7
  from enum import Enum
10
- from typing import Any, Dict, Generic, Optional, Type, Union, cast, get_origin
8
+ from typing import Dict, Generic, Optional, Type, Union, cast, get_origin
11
9
 
12
- from bluesky.protocols import Descriptor, Dtype, Reading
10
+ import numpy as np
11
+ from bluesky.protocols import DataKey, Dtype, Reading
13
12
 
14
- from ...utils import ReadingValueCallback, T, get_dtype
15
13
  from .signal_backend import SignalBackend
14
+ from .utils import DEFAULT_TIMEOUT, ReadingValueCallback, T, get_dtype
16
15
 
17
16
  primitive_dtypes: Dict[type, Dtype] = {
18
17
  str: "string",
@@ -22,7 +21,7 @@ primitive_dtypes: Dict[type, Dtype] = {
22
21
  }
23
22
 
24
23
 
25
- class SimConverter(Generic[T]):
24
+ class SoftConverter(Generic[T]):
26
25
  def value(self, value: T) -> T:
27
26
  return value
28
27
 
@@ -36,12 +35,17 @@ class SimConverter(Generic[T]):
36
35
  alarm_severity=-1 if severity > 2 else severity,
37
36
  )
38
37
 
39
- def descriptor(self, source: str, value) -> Descriptor:
38
+ def get_datakey(self, source: str, value) -> DataKey:
39
+ dtype = type(value)
40
+ if np.issubdtype(dtype, np.integer):
41
+ dtype = int
42
+ elif np.issubdtype(dtype, np.floating):
43
+ dtype = float
40
44
  assert (
41
- type(value) in primitive_dtypes
45
+ dtype in primitive_dtypes
42
46
  ), f"invalid converter for value of type {type(value)}"
43
- dtype = primitive_dtypes[type(value)]
44
- return dict(source=source, dtype=dtype, shape=[])
47
+ dtype_name = primitive_dtypes[dtype]
48
+ return {"source": source, "dtype": dtype_name, "shape": []}
45
49
 
46
50
  def make_initial_value(self, datatype: Optional[Type[T]]) -> T:
47
51
  if datatype is None:
@@ -50,9 +54,9 @@ class SimConverter(Generic[T]):
50
54
  return datatype()
51
55
 
52
56
 
53
- class SimArrayConverter(SimConverter):
54
- def descriptor(self, source: str, value) -> Descriptor:
55
- return dict(source=source, dtype="array", shape=[len(value)])
57
+ class SoftArrayConverter(SoftConverter):
58
+ def get_datakey(self, source: str, value) -> DataKey:
59
+ return {"source": source, "dtype": "array", "shape": [len(value)]}
56
60
 
57
61
  def make_initial_value(self, datatype: Optional[Type[T]]) -> T:
58
62
  if datatype is None:
@@ -65,7 +69,7 @@ class SimArrayConverter(SimConverter):
65
69
 
66
70
 
67
71
  @dataclass
68
- class SimEnumConverter(SimConverter):
72
+ class SoftEnumConverter(SoftConverter):
69
73
  enum_class: Type[Enum]
70
74
 
71
75
  def write_value(self, value: Union[Enum, str]) -> Enum:
@@ -74,11 +78,9 @@ class SimEnumConverter(SimConverter):
74
78
  else:
75
79
  return self.enum_class(value)
76
80
 
77
- def descriptor(self, source: str, value) -> Descriptor:
81
+ def get_datakey(self, source: str, value) -> DataKey:
78
82
  choices = [e.value for e in self.enum_class]
79
- return dict(
80
- source=source, dtype="string", shape=[], choices=choices
81
- ) # type: ignore
83
+ return {"source": source, "dtype": "string", "shape": [], "choices": choices} # type: ignore
82
84
 
83
85
  def make_initial_value(self, datatype: Optional[Type[T]]) -> T:
84
86
  if datatype is None:
@@ -87,48 +89,50 @@ class SimEnumConverter(SimConverter):
87
89
  return cast(T, list(datatype.__members__.values())[0]) # type: ignore
88
90
 
89
91
 
90
- class DisconnectedSimConverter(SimConverter):
91
- def __getattribute__(self, __name: str) -> Any:
92
- raise NotImplementedError("No PV has been set as connect() has not been called")
93
-
94
-
95
92
  def make_converter(datatype):
96
93
  is_array = get_dtype(datatype) is not None
97
94
  is_sequence = get_origin(datatype) == abc.Sequence
98
95
  is_enum = issubclass(datatype, Enum) if inspect.isclass(datatype) else False
99
96
 
100
97
  if is_array or is_sequence:
101
- return SimArrayConverter()
98
+ return SoftArrayConverter()
102
99
  if is_enum:
103
- return SimEnumConverter(datatype)
100
+ return SoftEnumConverter(datatype)
104
101
 
105
- return SimConverter()
102
+ return SoftConverter()
106
103
 
107
104
 
108
- class SimSignalBackend(SignalBackend[T]):
109
- """An simulated backend to a Signal, created with ``Signal.connect(sim=True)``"""
105
+ class SoftSignalBackend(SignalBackend[T]):
106
+ """An backend to a soft Signal, for test signals see ``MockSignalBackend``."""
110
107
 
111
108
  _value: T
112
- _initial_value: T
109
+ _initial_value: Optional[T]
113
110
  _timestamp: float
114
111
  _severity: int
115
112
 
116
- def __init__(self, datatype: Optional[Type[T]], source: str) -> None:
117
- pv = re.split(r"://", source)[-1]
118
- self.source = f"sim://{pv}"
113
+ def __init__(
114
+ self,
115
+ datatype: Optional[Type[T]],
116
+ initial_value: Optional[T] = None,
117
+ ) -> None:
119
118
  self.datatype = datatype
120
- self.pv = source
121
- self.converter: SimConverter = DisconnectedSimConverter()
122
- self.put_proceeds = asyncio.Event()
123
- self.put_proceeds.set()
124
- self.callback: Optional[ReadingValueCallback[T]] = None
119
+ self._initial_value = initial_value
120
+ self.converter: SoftConverter = make_converter(datatype)
121
+ if self._initial_value is None:
122
+ self._initial_value = self.converter.make_initial_value(self.datatype)
123
+ else:
124
+ self._initial_value = self.converter.write_value(self._initial_value)
125
125
 
126
- async def connect(self) -> None:
127
- self.converter = make_converter(self.datatype)
128
- self._initial_value = self.converter.make_initial_value(self.datatype)
126
+ self.callback: Optional[ReadingValueCallback[T]] = None
129
127
  self._severity = 0
128
+ self.set_value(self._initial_value)
129
+
130
+ def source(self, name: str) -> str:
131
+ return f"soft://{name}"
130
132
 
131
- await self.put(None)
133
+ async def connect(self, timeout: float = DEFAULT_TIMEOUT) -> None:
134
+ """Connection isn't required for soft signals."""
135
+ pass
132
136
 
133
137
  async def put(self, value: Optional[T], wait=True, timeout=None):
134
138
  write_value = (
@@ -136,13 +140,11 @@ class SimSignalBackend(SignalBackend[T]):
136
140
  if value is not None
137
141
  else self._initial_value
138
142
  )
139
- self._set_value(write_value)
140
143
 
141
- if wait:
142
- await asyncio.wait_for(self.put_proceeds.wait(), timeout)
144
+ self.set_value(write_value)
143
145
 
144
- def _set_value(self, value: T):
145
- """Method to bypass asynchronous logic, designed to only be used in tests."""
146
+ def set_value(self, value: T):
147
+ """Method to bypass asynchronous logic."""
146
148
  self._value = value
147
149
  self._timestamp = time.monotonic()
148
150
  reading: Reading = self.converter.reading(
@@ -152,8 +154,8 @@ class SimSignalBackend(SignalBackend[T]):
152
154
  if self.callback:
153
155
  self.callback(reading, self._value)
154
156
 
155
- async def get_descriptor(self) -> Descriptor:
156
- return self.converter.descriptor(self.source, self._value)
157
+ async def get_datakey(self, source: str) -> DataKey:
158
+ return self.converter.get_datakey(source, self._value)
157
159
 
158
160
  async def get_reading(self) -> Reading:
159
161
  return self.converter.reading(self._value, self._timestamp, self._severity)
@@ -161,6 +163,10 @@ class SimSignalBackend(SignalBackend[T]):
161
163
  async def get_value(self) -> T:
162
164
  return self.converter.value(self._value)
163
165
 
166
+ async def get_setpoint(self) -> T:
167
+ """For a soft signal, the setpoint and readback values are the same."""
168
+ return await self.get_value()
169
+
164
170
  def set_callback(self, callback: Optional[ReadingValueCallback[T]]) -> None:
165
171
  if callback:
166
172
  assert not self.callback, "Cannot set a callback when one is already set"