diff --git a/concert/base.py b/concert/base.py index f57f0076f..98f1112de 100644 --- a/concert/base.py +++ b/concert/base.py @@ -247,7 +247,7 @@ async def call_func(instance, *args, **kwargs): return wrapped -def check(source='*', target='*'): +def check(source='*', target='*', device_state='state'): """ Decorates a method for checking the device state. @@ -255,9 +255,11 @@ def check(source='*', target='*'): invoking the decorated method. *target* is the state that the state object will be after successful completion of the method or a list of possible target states. + + By setting *device_state* you can specify which state object to use for. """ async def check_now(instance, allowed_states, state_str): - state = await instance['state'].get() + state = await instance[device_state].get() if state not in allowed_states and '*' not in allowed_states: raise TransitionNotAllowed(f"{state_str} state `{state}' not in `{allowed_states}'") diff --git a/concert/devices/cameras/base.py b/concert/devices/cameras/base.py index faa6ec170..208caacfd 100644 --- a/concert/devices/cameras/base.py +++ b/concert/devices/cameras/base.py @@ -91,6 +91,7 @@ class Camera(Device): trigger_sources = Bunch(['AUTO', 'SOFTWARE', 'EXTERNAL']) trigger_types = Bunch(['EDGE', 'LEVEL']) state = State(default='standby') + live_state = State(default='standby') frame_rate = Quantity(1 / q.second, help="Frame frequency") trigger_source = Parameter(help="Trigger source") @@ -118,6 +119,26 @@ async def stop_recording(self): """ await self._stop_real() + @background + @check(source='standby', target='recording', device_state='live_state') + async def start_recording_live(self): + """ + start_recording() + + Start recording frames. + """ + await self._record_live_real() + + @background + @check(source='recording', target='standby', device_state='live_state') + async def stop_recording_live(self): + """ + stop_recording() + + Stop recording frames. + """ + await self._stop_live_real() + @contextlib.asynccontextmanager async def recording(self): """ @@ -143,12 +164,21 @@ async def trigger(self): await self._trigger_real() @background + @check(source='recording') async def grab(self) -> ImageWithMetadata: """Return a concert.storage.ImageWithMetadata (subclass of np.ndarray) with data of the current frame.""" img = self.convert(await self._grab_real()) return img.view(ImageWithMetadata) + @background + @check(source='recording', device_state='live_state') + async def grab_live(self) -> ImageWithMetadata: + """Return a concert.storage.ImageWithMetadata (subclass of np.ndarray) with data of the + current frame.""" + img = self.convert(await self._grab_live_real()) + return img.view(ImageWithMetadata) + async def stream(self): """ stream() @@ -157,10 +187,30 @@ async def stream(self): """ await self.set_trigger_source(self.trigger_sources.AUTO) await self.start_recording() + async for f in self.yield_frames(): + yield f + + async def stream_live(self): + """ + stream_live() + + Grab live frames and continuously yield them. This is an async generator. + """ + # Assuming that the live_view is always some kind of auto-trigger mode I would remove this + # await self.set_trigger_source(self.trigger_sources.AUTO) + await self.start_recording_live() + + async for f in self.yield_frames_live(): + yield f + async def yield_frames(self): while await self.get_state() == 'recording': yield await self.grab() + async def yield_frames_live(self): + while await self.get_live_state() == 'recording': + yield await self.grab_live() + async def _get_trigger_source(self): raise AccessorNotImplementedError @@ -173,12 +223,21 @@ async def _record_real(self): async def _stop_real(self): raise AccessorNotImplementedError + async def _record_live_real(self): + raise AccessorNotImplementedError + + async def _stop_live_real(self): + raise AccessorNotImplementedError + async def _trigger_real(self): raise AccessorNotImplementedError async def _grab_real(self): raise AccessorNotImplementedError + async def _grab_live_real(self): + raise AccessorNotImplementedError + class BufferedMixin(Device): diff --git a/concert/devices/cameras/uca.py b/concert/devices/cameras/uca.py index bec5e50fb..59914bf57 100644 --- a/concert/devices/cameras/uca.py +++ b/concert/devices/cameras/uca.py @@ -7,7 +7,7 @@ from concert.coroutines.base import background, run_in_executor from concert.quantities import q from concert.base import Parameter, Quantity -from concert.helpers import Bunch +from concert.helpers import Bunch, ImageWithMetadata from concert.devices.cameras import base @@ -167,8 +167,10 @@ async def stop_readout(self): self.uca.stop_readout() @background - async def grab(self, index=None): - return self.convert(await self._grab_real(index)) + async def grab(self, index=None) -> ImageWithMetadata: + img = self.convert(await self._grab_real(index)) + + return img.view(ImageWithMetadata) def write(self, name, data): """Write NumPy array *data* for *name*.""" @@ -216,6 +218,10 @@ async def _grab_real(self, index=None): return array + @_translate_gerror + async def _grab_live_real(self): + return await self._grab_real(index=None) + async def _determine_shape_for_grab(self): self._record_shape = ((await self.get_roi_height()).magnitude, (await self.get_roi_width()).magnitude)