Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions concert/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -247,17 +247,19 @@ 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.

*source* denotes the source state that must be present at the time of
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}'")

Expand Down
59 changes: 59 additions & 0 deletions concert/devices/cameras/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")

Expand Down Expand Up @@ -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):
"""
Expand All @@ -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()
Expand All @@ -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

Expand All @@ -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):

Expand Down
12 changes: 9 additions & 3 deletions concert/devices/cameras/uca.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down Expand Up @@ -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*."""
Expand Down Expand Up @@ -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)
Expand Down