Example #1
0
    def __init__(self,
                 attached_instruments: Dict[types.Mount, Dict[str,
                                                              Optional[str]]],
                 attached_modules: List[str],
                 config,
                 loop,
                 strict_attached_instruments=True) -> None:
        """ Build the simulator.

        :param attached_instruments: A dictionary describing the instruments
                                     the simulator should consider attached.
                                     If this argument is specified and
                                     :py:meth:`get_attached_instruments` is
                                     called with expectations that do not
                                     match, the call fails. This is useful for
                                     making the simulator match the real
                                     hardware, for instance to check if a
                                     protocol asks for the right instruments.
                                     This dict should map mounts to either
                                     empty dicts or to dicts containing
                                     'model' and 'id' keys.
        :param attached_modules: A list of module model names (e.g.
                                 `'tempdeck'` or `'magdeck'`) representing
                                 modules the simulator should assume are
                                 attached. Like `attached_instruments`, used
                                 to make the simulator match the setup of the
                                 real hardware.
        :param config: The robot config to use
        :param loop: The asyncio event loop to use.
        :param strict_attached_instruments: This param changes the behavior of
                                            the instrument cache. If ``True``,
                                            (default), ``cache_instrument``
                                            calls requesting instruments not
                                            in ``attached_instruments`` will
                                            fail as if the instrument was not
                                            present. If ``False``, those calls
                                            will still pass but give a response
                                            version of 1, while calls
                                            requesting instruments that _are_
                                            present get the full number.
        """
        self._config = config
        self._loop = loop
        self._attached_instruments = attached_instruments
        self._attached_modules = [('mod' + str(idx), mod)
                                  for idx, mod in enumerate(attached_modules)]
        self._position = copy.copy(_HOME_POSITION)
        # Engaged axes start all true in smoothie for some reason so we
        # imitate that here
        # TODO(LC2642019) Create a simulating driver for smoothie instead of
        # using a flag
        self._smoothie_driver = SimulatingDriver()
        self._engaged_axes = {ax: True for ax in _HOME_POSITION}
        self._lights = {'button': False, 'rails': False}
        self._run_flag = Event()
        self._run_flag.set()
        self._log = MODULE_LOG.getChild(repr(self))
        self._strict_attached = bool(strict_attached_instruments)
Example #2
0
    def __init__(self,
                 attached_instruments: Dict[types.Mount, Dict[str,
                                                              Optional[str]]],
                 attached_modules: List[str],
                 config,
                 loop,
                 strict_attached_instruments=True) -> None:
        """ Build the simulator.

        :param attached_instruments: A dictionary describing the instruments
                                     the simulator should consider attached.
                                     If this argument is specified and
                                     :py:meth:`get_attached_instruments` is
                                     called with expectations that do not
                                     match, the call fails. This is useful for
                                     making the simulator match the real
                                     hardware, for instance to check if a
                                     protocol asks for the right instruments.
                                     This dict should map mounts to either
                                     empty dicts or to dicts containing
                                     'model' and 'id' keys.
        :param attached_modules: A list of module model names (e.g.
                                 `'tempdeck'` or `'magdeck'`) representing
                                 modules the simulator should assume are
                                 attached. Like `attached_instruments`, used
                                 to make the simulator match the setup of the
                                 real hardware.
        :param config: The robot config to use
        :param loop: The asyncio event loop to use.
        :param strict_attached_instruments: This param changes the behavior of
                                            the instrument cache. If ``True``,
                                            (default), ``cache_instrument``
                                            calls requesting instruments not
                                            in ``attached_instruments`` will
                                            fail as if the instrument was not
                                            present. If ``False``, those calls
                                            will still pass but give a response
                                            version of 1, while calls
                                            requesting instruments that _are_
                                            present get the full number.
        """
        self.config = config
        self._loop = loop

        def _sanitize_attached_instrument(
                passed_ai: Dict[str, Optional[str]] = None)\
                -> InstrumentSpec:
            if not passed_ai or not passed_ai.get('model'):
                return {'model': None, 'id': None}
            if passed_ai['model'] in config_models:
                return passed_ai  # type: ignore
            if passed_ai['model'] in config_names:
                return {
                    'model':
                    dummy_model_for_name(passed_ai['model']),  # type: ignore
                    'id': passed_ai.get('id')
                }
            raise KeyError('If you specify attached_instruments, the model '
                           'should be pipette names or pipette models, but '
                           f'{passed_ai["model"]} is not')

        self._attached_instruments = {
            m: _sanitize_attached_instrument(attached_instruments.get(m))
            for m in types.Mount
        }
        self._stubbed_attached_modules = attached_modules
        self._position = copy.copy(_HOME_POSITION)
        # Engaged axes start all true in smoothie for some reason so we
        # imitate that here
        # TODO(LC2642019) Create a simulating driver for smoothie instead of
        # using a flag
        self._smoothie_driver = SimulatingDriver()
        self._engaged_axes = {ax: True for ax in _HOME_POSITION}
        self._lights = {'button': False, 'rails': False}
        self._run_flag = Event()
        self._run_flag.set()
        self._log = MODULE_LOG.getChild(repr(self))
        self._strict_attached = bool(strict_attached_instruments)
        self._gpio_chardev = SimulatingGPIOCharDev('gpiochip0')
Example #3
0
class Simulator:
    """ This is a subclass of hardware_control that only simulates the
    hardware actions. It is suitable for use on a dev machine or on
    a robot with no smoothie connected.
    """
    def __init__(self,
                 attached_instruments: Dict[types.Mount, Dict[str,
                                                              Optional[str]]],
                 attached_modules: List[str],
                 config,
                 loop,
                 strict_attached_instruments=True) -> None:
        """ Build the simulator.

        :param attached_instruments: A dictionary describing the instruments
                                     the simulator should consider attached.
                                     If this argument is specified and
                                     :py:meth:`get_attached_instruments` is
                                     called with expectations that do not
                                     match, the call fails. This is useful for
                                     making the simulator match the real
                                     hardware, for instance to check if a
                                     protocol asks for the right instruments.
                                     This dict should map mounts to either
                                     empty dicts or to dicts containing
                                     'model' and 'id' keys.
        :param attached_modules: A list of module model names (e.g.
                                 `'tempdeck'` or `'magdeck'`) representing
                                 modules the simulator should assume are
                                 attached. Like `attached_instruments`, used
                                 to make the simulator match the setup of the
                                 real hardware.
        :param config: The robot config to use
        :param loop: The asyncio event loop to use.
        :param strict_attached_instruments: This param changes the behavior of
                                            the instrument cache. If ``True``,
                                            (default), ``cache_instrument``
                                            calls requesting instruments not
                                            in ``attached_instruments`` will
                                            fail as if the instrument was not
                                            present. If ``False``, those calls
                                            will still pass but give a response
                                            version of 1, while calls
                                            requesting instruments that _are_
                                            present get the full number.
        """
        self.config = config
        self._loop = loop

        def _sanitize_attached_instrument(
                passed_ai: Dict[str, Optional[str]] = None)\
                -> InstrumentSpec:
            if not passed_ai or not passed_ai.get('model'):
                return {'model': None, 'id': None}
            if passed_ai['model'] in config_models:
                return passed_ai  # type: ignore
            if passed_ai['model'] in config_names:
                return {
                    'model':
                    dummy_model_for_name(passed_ai['model']),  # type: ignore
                    'id': passed_ai.get('id')
                }
            raise KeyError('If you specify attached_instruments, the model '
                           'should be pipette names or pipette models, but '
                           f'{passed_ai["model"]} is not')

        self._attached_instruments = {
            m: _sanitize_attached_instrument(attached_instruments.get(m))
            for m in types.Mount
        }
        self._stubbed_attached_modules = attached_modules
        self._position = copy.copy(_HOME_POSITION)
        # Engaged axes start all true in smoothie for some reason so we
        # imitate that here
        # TODO(LC2642019) Create a simulating driver for smoothie instead of
        # using a flag
        self._smoothie_driver = SimulatingDriver()
        self._engaged_axes = {ax: True for ax in _HOME_POSITION}
        self._lights = {'button': False, 'rails': False}
        self._run_flag = Event()
        self._run_flag.set()
        self._log = MODULE_LOG.getChild(repr(self))
        self._strict_attached = bool(strict_attached_instruments)
        self._gpio_chardev = SimulatingGPIOCharDev('gpiochip0')

    @property
    def gpio_chardev(self) -> GPIODriverLike:
        return self._gpio_chardev

    async def setup_gpio_chardev(self):
        await self.gpio_chardev.setup()

    def update_position(self) -> Dict[str, float]:
        return self._position

    def move(self,
             target_position: Dict[str, float],
             home_flagged_axes: bool = True,
             speed: float = None,
             axis_max_speeds: Dict[str, float] = None):
        self._position.update(target_position)
        self._engaged_axes.update({ax: True for ax in target_position})

    def home(self, axes: List[str] = None) -> Dict[str, float]:
        # driver_3_0-> HOMED_POSITION
        checked_axes = axes or 'XYZABC'
        self._position.update({ax: _HOME_POSITION[ax] for ax in checked_axes})
        self._engaged_axes.update({ax: True for ax in checked_axes})
        return self._position

    def fast_home(self, axis: Sequence[str],
                  margin: float) -> Dict[str, float]:
        for ax in axis:
            self._position[ax] = _HOME_POSITION[ax]
            self._engaged_axes[ax] = True
        return self._position

    def _attached_to_mount(
            self, mount: types.Mount,
            expected_instr: Optional[PipetteName]) -> AttachedInstrument:
        init_instr = self._attached_instruments.get(mount, {
            'model': None,
            'id': None
        })
        found_model = init_instr['model']
        back_compat: List['PipetteName'] = []
        if found_model:
            back_compat = configs[found_model].get('backCompatNames', [])
        if expected_instr and found_model\
                and (not found_model.startswith(expected_instr)
                     and expected_instr not in back_compat):
            if self._strict_attached:
                raise RuntimeError(
                    'mount {}: expected instrument {} but got {}'.format(
                        mount.name, expected_instr, found_model))
            else:
                return {
                    'config': load(dummy_model_for_name(expected_instr)),
                    'id': None
                }
        elif found_model and expected_instr:
            # Instrument detected matches instrument expected (note:
            # "instrument detected" means passed as an argument to the
            # constructor of this class)
            return {
                'config': load(found_model, init_instr['id']),
                'id': init_instr['id']
            }
        elif found_model:
            # Instrument detected and no expected instrument specified
            return {
                'config': load(found_model, init_instr['id']),
                'id': init_instr['id']
            }
        elif expected_instr:
            # Expected instrument specified and no instrument detected
            return {
                'config': load(dummy_model_for_name(expected_instr)),
                'id': None
            }
        else:
            # No instrument detected or expected
            return {'config': None, 'id': None}

    def get_attached_instruments(
            self, expected: Dict[types.Mount,
                                 PipetteName]) -> AttachedInstruments:
        """ Update the internal cache of attached instruments.

        This method allows after-init-time specification of attached simulated
        instruments. The method will return
        - the instruments specified at init-time, or if those do not exists,
        - the instruments specified in expected, or if that is not passed,
        - nothing

        :param expected: A mapping of mount to instrument model prefixes. When
                         loading instruments from a prefix, we return the
                         lexically-first model that matches the prefix. If the
                         models specified in expected do not match the models
                         specified in the `attached_instruments` argument of
                         :py:meth:`__init__`, :py:attr:`RuntimeError` is
                         raised.
        :raises RuntimeError: If an instrument is expected but not found.
        :returns: A dict of mount to either instrument model names or `None`.
        """
        return {
            mount: self._attached_to_mount(mount, expected.get(mount))
            for mount in types.Mount
        }

    def set_active_current(self, axis_currents: Dict[Axis, float]):
        pass

    async def watch_modules(self, register_modules: RegisterModules):
        new_mods_at_ports = [
            modules.ModuleAtPort(port=f'/dev/ot_module_sim_{mod}{str(idx)}',
                                 name=mod)
            for idx, mod in enumerate(self._stubbed_attached_modules)
        ]
        await register_modules(new_mods_at_ports=new_mods_at_ports)

    @contextmanager
    def save_current(self):
        yield

    async def build_module(self,
                           port: str,
                           model: str,
                           interrupt_callback: modules.InterruptCallback,
                           loop: asyncio.AbstractEventLoop,
                           execution_manager: ExecutionManager,
                           sim_model: str = None) -> modules.AbstractModule:
        return await modules.build(port=port,
                                   which=model,
                                   simulating=True,
                                   interrupt_callback=interrupt_callback,
                                   loop=loop,
                                   execution_manager=execution_manager,
                                   sim_model=sim_model)

    @property
    def axis_bounds(self) -> Dict[Axis, Tuple[float, float]]:
        """ The (minimum, maximum) bounds for each axis. """
        return {
            Axis[ax]: (0, pos)
            for ax, pos in _HOME_POSITION.items() if ax not in 'BC'
        }

    @property
    def fw_version(self) -> Optional[str]:
        return 'Virtual Smoothie'

    async def update_fw_version(self):
        pass

    @property
    def board_revision(self) -> BoardRevision:
        return BoardRevision.OG

    async def update_firmware(self, filename, loop, modeset) -> str:
        return 'Did nothing (simulating)'

    def engaged_axes(self):
        return self._engaged_axes

    def disengage_axes(self, axes: List[str]):
        self._engaged_axes.update({ax: False for ax in axes})

    def set_lights(self, button: Optional[bool], rails: Optional[bool]):
        if button is not None:
            self._lights['button'] = button
        if rails is not None:
            self._lights['rails'] = rails

    def get_lights(self) -> Dict[str, bool]:
        return self._lights

    def pause(self):
        self._run_flag.clear()

    def resume(self):
        self._run_flag.set()

    def halt(self):
        self._run_flag.set()

    def hard_halt(self):
        self._run_flag.set()

    def probe(self, axis: str, distance: float) -> Dict[str, float]:
        self._position[axis.upper()] = self._position[axis.upper()] + distance
        return self._position

    def clean_up(self):
        pass

    def configure_mount(self, mount: types.Mount,
                        config: InstrumentHardwareConfigs):
        mount_axis = Axis.by_mount(mount)
        plunger_axis = Axis.of_plunger(mount)

        self._smoothie_driver.update_steps_per_mm(
            {plunger_axis.name: config['steps_per_mm']})
        self._smoothie_driver.update_pipette_config(
            mount_axis.name, {'home': config['home_pos']})
        self._smoothie_driver.update_pipette_config(
            plunger_axis.name, {'max_travel': config['max_travel']})
        self._smoothie_driver.set_dwelling_current(
            {plunger_axis.name: config['idle_current']})
        ms = config['splits']
        if ms:
            self._smoothie_driver.configure_splits_for({plunger_axis.name: ms})