Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Split out common AD file plugin logic into core writer class, create ADTiffWriter #606

Open
wants to merge 31 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
652de13
Starting to work on ad tiff writer
jwlodek Sep 4, 2024
e289ee4
Resolve merge conflicts
jwlodek Oct 1, 2024
f36ec3a
Continue working on tiff writer
jwlodek Oct 8, 2024
83dff62
Further work on tiff writer, existing tests now passing.
jwlodek Oct 8, 2024
1a52a21
Remove functions moved to superclas from hdf writer
jwlodek Oct 8, 2024
489cfd8
Significant re-org and simplification of ad classes
jwlodek Oct 9, 2024
83c6884
Ruff formatting
jwlodek Oct 9, 2024
3b4f45a
Modify ad sim classes to reflect new superclasses
jwlodek Oct 9, 2024
7175b30
Modify vimba and kinetix classes
jwlodek Oct 9, 2024
faf53d6
Modify aravis and pilatus classes
jwlodek Oct 9, 2024
5b9f60f
Update all tests to make sure they still pass with changes
jwlodek Oct 10, 2024
8bbfd0e
Some cleanup
jwlodek Oct 10, 2024
1eab818
Merge with upstream
jwlodek Oct 10, 2024
f6825b4
Changes to standard detector to account for controller/writer types i…
jwlodek Nov 22, 2024
651b80d
Significant changes to base detector, controller, and writer classes
jwlodek Nov 22, 2024
38a61e8
Update detector and controller classes to reflect changes
jwlodek Nov 22, 2024
aecdf04
Make sure panda standard det uses new type hints
jwlodek Nov 22, 2024
e42fa12
Most tests passing
jwlodek Nov 22, 2024
07684a4
Merge with main and resolve conflicts
jwlodek Nov 22, 2024
6dc09f3
Revert change in test that was resolved by pydantic version update
jwlodek Nov 22, 2024
1f7dcd7
Remove debugging prints
jwlodek Nov 22, 2024
35dd1b1
Linter fixes
jwlodek Nov 22, 2024
8112220
Fix linter error
jwlodek Nov 22, 2024
ac1e509
Move creation of writer outside of base AreaDetector class init per r…
jwlodek Nov 26, 2024
8494da4
Make sure we don't wait for capture to be done!
jwlodek Nov 26, 2024
b212432
Merge with upstream
jwlodek Nov 26, 2024
3242d45
Merge with upstream
jwlodek Dec 9, 2024
488d7eb
Allow for specifying whether or not to use fileio signals for dataset…
jwlodek Dec 9, 2024
a76b70f
Revert "Allow for specifying whether or not to use fileio signals for…
jwlodek Dec 10, 2024
7da935e
Fix linter errors, remove unused enum
jwlodek Dec 10, 2024
0cd0ddf
Change from return to await to conform to return type
jwlodek Dec 10, 2024
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
77 changes: 53 additions & 24 deletions src/ophyd_async/epics/adaravis/_aravis.py
Original file line number Diff line number Diff line change
@@ -1,24 +1,21 @@
from typing import get_args
from collections.abc import Sequence
from typing import cast, get_args

from bluesky.protocols import HasHints, Hints

from ophyd_async.core import PathProvider, StandardDetector
from ophyd_async.core import PathProvider
from ophyd_async.core._signal import SignalR
from ophyd_async.epics import adcore

from ._aravis_controller import AravisController
from ._aravis_io import AravisDriverIO


class AravisDetector(StandardDetector, HasHints):
class AravisDetector(adcore.AreaDetector):
"""
Ophyd-async implementation of an ADAravis Detector.
The detector may be configured for an external trigger on a GPIO port,
which must be done prior to preparing the detector
"""

_controller: AravisController
_writer: adcore.ADHDFWriter

def __init__(
self,
prefix: str,
Expand All @@ -27,24 +24,28 @@ def __init__(
hdf_suffix="HDF1:",
name="",
gpio_number: AravisController.GPIO_NUMBER = 1,
config_sigs: Sequence[SignalR] = (),
):
self.drv = AravisDriverIO(prefix + drv_suffix)
self.hdf = adcore.NDFileHDFIO(prefix + hdf_suffix)

super().__init__(
AravisController(self.drv, gpio_number=gpio_number),
adcore.ADHDFWriter(
self.hdf,
path_provider,
lambda: self.name,
adcore.ADBaseDatasetDescriber(self.drv),
),
config_sigs=(self.drv.acquire_time,),
prefix,
path_provider,
adcore.ADHDFWriter,
hdf_suffix,
AravisController,
AravisDriverIO,
drv_suffix=drv_suffix,
name=name,
config_sigs=config_sigs,
gpio_number=gpio_number,
)
self.hdf = self._fileio

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not a big fan of inserting another class into the heirarchy with many more arguments, could we move out the creation of the driver and writer classes into a utility function instead? I had a go with this idea and came up with:

class AravisDetectorTIFF(StandardDetector[AravisController]):
    def __init__(
        self,
        prefix: str,
        path_provider: PathProvider,
        drv_suffix="cam1:",
        tiff_suffix="TIFF1:",
        gpio_number: AravisController.GPIO_NUMBER = 1,
        config_sigs: Sequence[SignalR] = (),
        name="",
    ):
        self.drv, self.tiff, writer = adcore.areadetector_driver_and_tiff(
            drv_cls=AravisDriverIO,
            prefix=prefix,
            drv_suffix=drv_suffix,
            fileio_suffix=tiff_suffix,
            path_provider=path_provider,
        )
        super().__init__(
            controller=AravisController(self.drv, gpio_number),
            writer=writer,
            config_sigs=config_sigs,
            name=name,
        )

I did some of the changes required so pyright was happy, but didn't make the tests work:
efd86c9

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it be possible to have the utility function(s) be factories that take in a drv_cls and writer_cls? It would be nice if we didn't need to make a function for each writer type. I am OK with that approach generally though

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That sounds reasonable, I wonder if we should stretch it even further and say that we make one AravisDetector that has an enum init arg fileio_type that switches between HDF and TIFF?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That would be ideal I think. I'll work on getting something like that working.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmmm, not sure I love that, but it would be nice to be able to specify the writer and have a default suffix. What if fileio_suffix=None, and

if fileio_suffix is None:
    fileio_suffix = ad_writer_type.value

And have the enum values for the writer types just correspond to the default suffixes?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Otherwise we use whatever the suffix kwarg provides?

Copy link
Collaborator

@coretl coretl Nov 19, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Found another possible approach which avoids the need to make detector specific subclasses:

ADBaseIOT = TypeVar("ADBaseIOT", bound=ADBaseIO)
NDFileIOT = TypeVar("NDFileIOT", bound=NDFileIO)
NDPluginBaseIOT = TypeVar("NDPluginBaseIOT", bound=NDPluginBaseIO)


class AreaDetector(
    Generic[ADBaseIOT, DetectorControllerT, NDFileIOT],
    StandardDetector[DetectorControllerT],
):
    def __init__(
        self,
        drv: ADBaseIOT,
        controller: DetectorControllerT,
        fileio: NDFileIOT,
        writer: DetectorWriter,
        plugins: dict[str, NDPluginBaseIO],
        config_sigs: Sequence[SignalR] = (),
        name: str = "",
    ):
        self.drv = drv
        self.fileio = fileio
        for name, plugin in plugins.items():
            setattr(self, name, plugin)
        super().__init__(controller, writer, config_sigs, name)

    def get_plugin(
        self, name: str, plugin_type: type[NDPluginBaseIOT] = NDPluginBaseIO
    ) -> NDPluginBaseIOT:
        plugin = getattr(self, name, None)
        if not isinstance(plugin, plugin_type):
            raise TypeError(
                f"Expected {self.name}.{name} to be a {plugin_type}, got {plugin}"
            )
        return plugin

    @classmethod
    def with_hdf(
        cls,
        drv: ADBaseIOT,
        controller: DetectorControllerT,
        hdf_prefix: str,
        path_provider: PathProvider,
        plugins: dict[str, NDPluginBaseIO],
        config_sigs: Sequence[SignalR] = (),
        name: str = "",
    ) -> AreaDetector[ADBaseIOT, DetectorControllerT, NDFileHDFIO]:
        fileio = NDFileHDFIO(hdf_prefix)
        writer = ADHDFWriter(
            fileio,
            path_provider,
            lambda: det.name,
            ADBaseDatasetDescriber(drv),
            *plugins.values(),
        )
        det = AreaDetector(drv, controller, fileio, writer, plugins, config_sigs, name)
        return det

    @classmethod
    def with_tiff(
        cls,
        drv: ADBaseIOT,
        controller: DetectorControllerT,
        tiff_prefix: str,
        path_provider: PathProvider,
        plugins: dict[str, NDPluginBaseIO],
        config_sigs: Sequence[SignalR] = (),
        name: str = "",
    ) -> AreaDetector[ADBaseIOT, DetectorControllerT, NDFileIO]:
        fileio = NDFileIO(tiff_prefix)
        writer = ADTIFFWriter(
            fileio, path_provider, lambda: det.name, ADBaseDatasetDescriber(drv)
        )
        det = AreaDetector(drv, controller, fileio, writer, plugins, config_sigs, name)
        return det

So now det.drv and det.fileio are always present, and we can do det.get_plugin("stats", NDPluginStatsIO) if we created it with AreaDetector(..., plugins={"stats": NDPluginStatsIO(prefix+"STATS")}

Copy link
Collaborator

@coretl coretl Nov 19, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So the detector specific classes turn into helper functions:

def _aravis_drv_controller(
    prefix: str, drv_suffix: str, gpio_number: AravisController.GPIO_NUMBER
) -> tuple[AravisDriverIO, AravisController]:
    drv = AravisDriverIO(prefix + drv_suffix)
    controller = AravisController(drv, gpio_number)
    return drv, controller


def aravis_detector_hdf(
    prefix: str,
    path_provider: PathProvider,
    drv_suffix="cam1:",
    hdf_suffix="HDF1:",
    gpio_number: AravisController.GPIO_NUMBER = 1,
    plugins: dict[str, NDPluginBaseIO] | None = None,
    config_sigs: Sequence[SignalR] = (),
    name="",
) -> adcore.AreaDetector:
    drv, controller = _aravis_drv_controller(prefix, drv_suffix, gpio_number)
    hdf_prefix = prefix + hdf_suffix
    return adcore.AreaDetector.with_hdf(
        drv, controller, hdf_prefix, path_provider, plugins or {}, config_sigs, name
    )


def aravis_detector_tiff(
    prefix: str,
    path_provider: PathProvider,
    drv_suffix="cam1:",
    tiff_suffix="TIFF:",
    gpio_number: AravisController.GPIO_NUMBER = 1,
    plugins: dict[str, NDPluginBaseIO] | None = None,
    config_sigs: Sequence[SignalR] = (),
    name="",
) -> adcore.AreaDetector:
    drv, controller = _aravis_drv_controller(prefix, drv_suffix, gpio_number)
    tiff_prefix = prefix + tiff_suffix
    return adcore.AreaDetector.with_hdf(
        drv, controller, tiff_prefix, path_provider, plugins or {}, config_sigs, name
    )

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pushed again to jwlodek#2

@property
def controller(self) -> AravisController:
return cast(AravisController, self._controller)

def get_external_trigger_gpio(self):
return self._controller.gpio_number
return self.controller.gpio_number

def set_external_trigger_gpio(self, gpio_number: AravisController.GPIO_NUMBER):
supported_gpio_numbers = get_args(AravisController.GPIO_NUMBER)
Expand All @@ -54,8 +55,36 @@ def set_external_trigger_gpio(self, gpio_number: AravisController.GPIO_NUMBER):
f"indices: {supported_gpio_numbers} but was asked to "
f"use {gpio_number}"
)
self._controller.gpio_number = gpio_number
self.controller.gpio_number = gpio_number

@property
def hints(self) -> Hints:
return self._writer.hints

class AravisDetectorTIFF(adcore.AreaDetector):
"""
Ophyd-async implementation of an ADAravis Detector.
The detector may be configured for an external trigger on a GPIO port,
which must be done prior to preparing the detector
"""

def __init__(
self,
prefix: str,
path_provider: PathProvider,
drv_suffix="cam1:",
hdf_suffix="HDF1:",
name="",
gpio_number: AravisController.GPIO_NUMBER = 1,
config_sigs: Sequence[SignalR] = (),
):
super().__init__(
prefix,
path_provider,
adcore.ADTIFFWriter,
hdf_suffix,
AravisController,
AravisDriverIO,
drv_suffix=drv_suffix,
name=name,
config_sigs=config_sigs,
gpio_number=gpio_number,
)
self.tiff = self._fileio
34 changes: 12 additions & 22 deletions src/ophyd_async/epics/adaravis/_aravis_controller.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,10 @@
import asyncio
from typing import Literal
from typing import Literal, cast

from ophyd_async.core import (
DetectorController,
DetectorTrigger,
TriggerInfo,
set_and_wait_for_value,
)
from ophyd_async.core._status import AsyncStatus
from ophyd_async.epics import adcore

from ._aravis_io import AravisDriverIO, AravisTriggerMode, AravisTriggerSource
Expand All @@ -18,13 +15,16 @@
_HIGHEST_POSSIBLE_DEADTIME = 1961e-6


class AravisController(DetectorController):
class AravisController(adcore.ADBaseController):
GPIO_NUMBER = Literal[1, 2, 3, 4]

def __init__(self, driver: AravisDriverIO, gpio_number: GPIO_NUMBER) -> None:
self._drv = driver
super().__init__(driver)
self.gpio_number = gpio_number
self._arm_status: AsyncStatus | None = None

@property
def driver(self) -> AravisDriverIO:
return cast(AravisDriverIO, self._driver)

def get_deadtime(self, exposure: float | None) -> float:
return _HIGHEST_POSSIBLE_DEADTIME
Expand All @@ -35,25 +35,18 @@ async def prepare(self, trigger_info: TriggerInfo):
else:
image_mode = adcore.ImageMode.multiple
if (exposure := trigger_info.livetime) is not None:
await self._drv.acquire_time.set(exposure)
await self.driver.acquire_time.set(exposure)

trigger_mode, trigger_source = self._get_trigger_info(trigger_info.trigger)
# trigger mode must be set first and on it's own!
await self._drv.trigger_mode.set(trigger_mode)
await self.driver.trigger_mode.set(trigger_mode)

await asyncio.gather(
self._drv.trigger_source.set(trigger_source),
self._drv.num_images.set(trigger_info.total_number_of_triggers),
self._drv.image_mode.set(image_mode),
self.driver.trigger_source.set(trigger_source),
self.driver.num_images.set(trigger_info.total_number_of_triggers),
self.driver.image_mode.set(image_mode),
)

async def arm(self):
self._arm_status = await set_and_wait_for_value(self._drv.acquire, True)

async def wait_for_idle(self):
if self._arm_status:
await self._arm_status

def _get_trigger_info(
self, trigger: DetectorTrigger
) -> tuple[AravisTriggerMode, AravisTriggerSource]:
Expand All @@ -72,6 +65,3 @@ def _get_trigger_info(
return AravisTriggerMode.off, "Freerun"
else:
return (AravisTriggerMode.on, f"Line{self.gpio_number}") # type: ignore

async def disarm(self):
await adcore.stop_busy_record(self._drv.acquire, False, timeout=1)
18 changes: 10 additions & 8 deletions src/ophyd_async/epics/adcore/__init__.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,17 @@
from ._core_detector import AreaDetector
from ._core_io import (
ADBaseIO,
DetectorState,
NDArrayBaseIO,
NDFileHDFIO,
NDFileIO,
NDPluginStatsIO,
)
from ._core_logic import (
DEFAULT_GOOD_STATES,
ADBaseDatasetDescriber,
set_exposure_time_and_acquire_period_if_supplied,
start_acquiring_driver_and_ensure_status,
)
from ._core_logic import DEFAULT_GOOD_STATES, ADBaseController, ADBaseDatasetDescriber
from ._core_writer import ADWriter
from ._hdf_writer import ADHDFWriter
from ._single_trigger import SingleTriggerDetector
from ._tiff_writer import ADTIFFWriter
from ._utils import (
ADBaseDataType,
FileWriteMode,
Expand All @@ -28,13 +27,16 @@
"ADBaseIO",
"DetectorState",
"NDArrayBaseIO",
"NDFileIO",
"NDFileHDFIO",
"NDPluginStatsIO",
"DEFAULT_GOOD_STATES",
"ADBaseDatasetDescriber",
"set_exposure_time_and_acquire_period_if_supplied",
"start_acquiring_driver_and_ensure_status",
"ADBaseController",
"AreaDetector",
"ADWriter",
"ADHDFWriter",
"ADTIFFWriter",
"SingleTriggerDetector",
"ADBaseDataType",
"FileWriteMode",
Expand Down
66 changes: 66 additions & 0 deletions src/ophyd_async/epics/adcore/_core_detector.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
from collections.abc import Sequence
from typing import cast

from bluesky.protocols import HasHints, Hints

from ophyd_async.core import PathProvider, SignalR, StandardDetector

from ._core_io import ADBaseIO, NDFileHDFIO, NDFileIO
from ._core_logic import ADBaseController, ADBaseDatasetDescriber
from ._core_writer import ADWriter
from ._hdf_writer import ADHDFWriter
from ._tiff_writer import ADTIFFWriter


def get_io_class_for_writer(writer_class: type[ADWriter]):
writer_to_io_map = {
ADWriter: NDFileIO,
ADHDFWriter: NDFileHDFIO,
ADTIFFWriter: NDFileIO,
}
return writer_to_io_map[writer_class]


class AreaDetector(StandardDetector, HasHints):
_controller: ADBaseController
_writer: ADWriter

def __init__(
self,
prefix: str,
path_provider: PathProvider,
writer_class: type[ADWriter] = ADWriter,
writer_suffix: str = "",
controller_class: type[ADBaseController] = ADBaseController,
drv_class: type[ADBaseIO] = ADBaseIO,
drv_suffix: str = "cam1:",
name: str = "",
config_sigs: Sequence[SignalR] = (),
**kwargs,
):
self.drv = drv_class(prefix + drv_suffix)
self._fileio = get_io_class_for_writer(writer_class)(prefix + writer_suffix)

super().__init__(
controller_class(self.drv, **kwargs),
writer_class(
self._fileio,
path_provider,
lambda: self.name,
ADBaseDatasetDescriber(self.drv),
),
config_sigs=(self.drv.acquire_period, self.drv.acquire_time, *config_sigs),
name=name,
)

@property
def controller(self) -> ADBaseController:
return cast(ADBaseController, self._controller)

@property
def writer(self) -> ADWriter:
return cast(ADWriter, self._writer)

@property
def hints(self) -> Hints:
return self._writer.hints
33 changes: 16 additions & 17 deletions src/ophyd_async/epics/adcore/_core_io.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,6 @@
from ._utils import ADBaseDataType, FileWriteMode, ImageMode


class Callback(str, Enum):
Enable = "Enable"
Disable = "Disable"


class NDArrayBaseIO(Device):
def __init__(self, prefix: str, name: str = "") -> None:
self.unique_id = epics_signal_r(int, prefix + "UniqueId_RBV")
Expand All @@ -32,9 +27,7 @@ def __init__(self, prefix: str, name: str = "") -> None:
class NDPluginBaseIO(NDArrayBaseIO):
def __init__(self, prefix: str, name: str = "") -> None:
self.nd_array_port = epics_signal_rw_rbv(str, prefix + "NDArrayPort")
self.enable_callbacks = epics_signal_rw_rbv(
Callback, prefix + "EnableCallbacks"
)
self.enable_callbacks = epics_signal_rw_rbv(bool, prefix + "EnableCallbacks")
self.nd_array_address = epics_signal_rw_rbv(int, prefix + "NDArrayAddress")
self.array_size0 = epics_signal_r(int, prefix + "ArraySize0_RBV")
self.array_size1 = epics_signal_r(int, prefix + "ArraySize1_RBV")
Expand Down Expand Up @@ -111,30 +104,36 @@ class Compression(str, Enum):
jpeg = "JPEG"


class NDFileHDFIO(NDPluginBaseIO):
class NDFileIO(NDPluginBaseIO):
def __init__(self, prefix: str, name="") -> None:
# Define some signals
self.position_mode = epics_signal_rw_rbv(bool, prefix + "PositionMode")
self.compression = epics_signal_rw_rbv(Compression, prefix + "Compression")
self.num_extra_dims = epics_signal_rw_rbv(int, prefix + "NumExtraDims")
self.file_path = epics_signal_rw_rbv(str, prefix + "FilePath")
self.file_name = epics_signal_rw_rbv(str, prefix + "FileName")
self.file_path_exists = epics_signal_r(bool, prefix + "FilePathExists_RBV")
self.file_template = epics_signal_rw_rbv(str, prefix + "FileTemplate")
self.full_file_name = epics_signal_r(str, prefix + "FullFileName_RBV")
self.file_number = epics_signal_rw(int, prefix + "FileNumber")
self.auto_increment = epics_signal_rw(bool, prefix + "AutoIncrement")
self.file_write_mode = epics_signal_rw_rbv(
FileWriteMode, prefix + "FileWriteMode"
)
self.num_capture = epics_signal_rw_rbv(int, prefix + "NumCapture")
self.num_captured = epics_signal_r(int, prefix + "NumCaptured_RBV")
self.swmr_mode = epics_signal_rw_rbv(bool, prefix + "SWMRMode")
self.lazy_open = epics_signal_rw_rbv(bool, prefix + "LazyOpen")
self.capture = epics_signal_rw_rbv(bool, prefix + "Capture")
self.flush_now = epics_signal_rw(bool, prefix + "FlushNow")
self.xml_file_name = epics_signal_rw_rbv(str, prefix + "XMLFileName")
self.array_size0 = epics_signal_r(int, prefix + "ArraySize0")
self.array_size1 = epics_signal_r(int, prefix + "ArraySize1")
self.create_directory = epics_signal_rw(int, prefix + "CreateDirectory")
super().__init__(prefix, name)


class NDFileHDFIO(NDFileIO):
def __init__(self, prefix: str, name="") -> None:
self.position_mode = epics_signal_rw_rbv(bool, prefix + "PositionMode")
self.compression = epics_signal_rw_rbv(Compression, prefix + "Compression")
self.num_extra_dims = epics_signal_rw_rbv(int, prefix + "NumExtraDims")
self.swmr_mode = epics_signal_rw_rbv(bool, prefix + "SWMRMode")
self.flush_now = epics_signal_rw(bool, prefix + "FlushNow")
self.xml_file_name = epics_signal_rw_rbv(str, prefix + "XMLFileName")
self.num_frames_chunks = epics_signal_r(int, prefix + "NumFramesChunks_RBV")
self.chunk_size_auto = epics_signal_rw_rbv(bool, prefix + "ChunkSizeAuto")
self.lazy_open = epics_signal_rw_rbv(bool, prefix + "LazyOpen")
super().__init__(prefix, name)
Loading
Loading