diff --git a/api/src/opentrons/drivers/heater_shaker/simulator.py b/api/src/opentrons/drivers/heater_shaker/simulator.py index ae90bc33027..8844d069cfa 100644 --- a/api/src/opentrons/drivers/heater_shaker/simulator.py +++ b/api/src/opentrons/drivers/heater_shaker/simulator.py @@ -1,4 +1,4 @@ -from typing import Dict +from typing import Dict, Optional from opentrons.util.async_helpers import ensure_yield from opentrons.drivers.heater_shaker.abstract import AbstractHeaterShakerDriver from opentrons.drivers.types import Temperature, RPM, HeaterShakerLabwareLatchStatus @@ -7,12 +7,13 @@ class SimulatingDriver(AbstractHeaterShakerDriver): DEFAULT_TEMP = 23 - def __init__(self) -> None: + def __init__(self, serial_number: Optional[str] = None) -> None: self._labware_latch_state = HeaterShakerLabwareLatchStatus.IDLE_UNKNOWN self._current_temperature = self.DEFAULT_TEMP self._temperature = Temperature(current=self.DEFAULT_TEMP, target=None) self._rpm = RPM(current=0, target=None) self._homing_status = True + self._serial_number = serial_number @ensure_yield async def connect(self) -> None: @@ -83,7 +84,7 @@ async def deactivate(self) -> None: @ensure_yield async def get_device_info(self) -> Dict[str, str]: return { - "serial": "dummySerialHS", + "serial": self._serial_number if self._serial_number else "dummySerialHS", "model": "dummyModelHS", "version": "dummyVersionHS", } diff --git a/api/src/opentrons/drivers/mag_deck/simulator.py b/api/src/opentrons/drivers/mag_deck/simulator.py index 1b8bc545bf4..303711ce6c2 100644 --- a/api/src/opentrons/drivers/mag_deck/simulator.py +++ b/api/src/opentrons/drivers/mag_deck/simulator.py @@ -11,9 +11,12 @@ class SimulatingDriver(AbstractMagDeckDriver): - def __init__(self, sim_model: Optional[str] = None) -> None: + def __init__( + self, sim_model: Optional[str] = None, serial_number: Optional[str] = None + ) -> None: self._height = 0.0 self._model = MAG_DECK_MODELS[sim_model] if sim_model else "mag_deck_v1.1" + self._serial_number = serial_number @ensure_yield async def probe_plate(self) -> None: @@ -30,7 +33,7 @@ async def move(self, location: float) -> None: @ensure_yield async def get_device_info(self) -> Dict[str, str]: return { - "serial": "dummySerialMD", + "serial": self._serial_number if self._serial_number else "dummySerialMD", "model": self._model, "version": "dummyVersionMD", } diff --git a/api/src/opentrons/drivers/rpi_drivers/interfaces.py b/api/src/opentrons/drivers/rpi_drivers/interfaces.py index 3923b250a27..f3986ae78d7 100644 --- a/api/src/opentrons/drivers/rpi_drivers/interfaces.py +++ b/api/src/opentrons/drivers/rpi_drivers/interfaces.py @@ -1,12 +1,15 @@ -from typing import List +from typing import List, Union from typing_extensions import Protocol -from opentrons.hardware_control.modules.types import ModuleAtPort +from opentrons.hardware_control.modules.types import ( + ModuleAtPort, + SimulatingModuleAtPort, +) class USBDriverInterface(Protocol): def match_virtual_ports( self, - virtual_port: List[ModuleAtPort], - ) -> List[ModuleAtPort]: + virtual_port: Union[List[ModuleAtPort], List[SimulatingModuleAtPort]], + ) -> Union[List[ModuleAtPort], List[SimulatingModuleAtPort]]: ... diff --git a/api/src/opentrons/drivers/rpi_drivers/usb.py b/api/src/opentrons/drivers/rpi_drivers/usb.py index 499284368e0..04ee5496c4a 100644 --- a/api/src/opentrons/drivers/rpi_drivers/usb.py +++ b/api/src/opentrons/drivers/rpi_drivers/usb.py @@ -8,9 +8,12 @@ import subprocess import re import os -from typing import List +from typing import List, Union -from opentrons.hardware_control.modules.types import ModuleAtPort +from opentrons.hardware_control.modules.types import ( + ModuleAtPort, + SimulatingModuleAtPort, +) from opentrons.hardware_control.types import BoardRevision from .interfaces import USBDriverInterface @@ -79,8 +82,8 @@ def _read_usb_bus(self) -> List[USBPort]: def match_virtual_ports( self, - virtual_ports: List[ModuleAtPort], - ) -> List[ModuleAtPort]: + virtual_ports: Union[List[ModuleAtPort], List[SimulatingModuleAtPort]], + ) -> Union[List[ModuleAtPort], List[SimulatingModuleAtPort]]: """ Match Virtual Ports @@ -89,7 +92,7 @@ def match_virtual_ports( the physical usb port information. The virtual port path looks something like: dev/ot_module_[MODULE NAME] - :param virtual_ports: A list of ModuleAtPort objects + :param virtual_ports: A list of ModuleAtPort or SimulatingModuleAtPort objects that hold the name and virtual port of each module connected to the robot. diff --git a/api/src/opentrons/drivers/rpi_drivers/usb_simulator.py b/api/src/opentrons/drivers/rpi_drivers/usb_simulator.py index d3931c00fdd..be7cec2e48e 100644 --- a/api/src/opentrons/drivers/rpi_drivers/usb_simulator.py +++ b/api/src/opentrons/drivers/rpi_drivers/usb_simulator.py @@ -4,15 +4,19 @@ A class to convert info from the usb bus into a more readable format. """ -from typing import List +from typing import List, Union -from opentrons.hardware_control.modules.types import ModuleAtPort +from opentrons.hardware_control.modules.types import ( + ModuleAtPort, + SimulatingModuleAtPort, +) from .interfaces import USBDriverInterface class USBBusSimulator(USBDriverInterface): def match_virtual_ports( - self, virtual_port: List[ModuleAtPort] - ) -> List[ModuleAtPort]: + self, + virtual_port: Union[List[ModuleAtPort], List[SimulatingModuleAtPort]], + ) -> Union[List[ModuleAtPort], List[SimulatingModuleAtPort]]: return virtual_port diff --git a/api/src/opentrons/drivers/temp_deck/simulator.py b/api/src/opentrons/drivers/temp_deck/simulator.py index efce88ea234..09a4f791e01 100644 --- a/api/src/opentrons/drivers/temp_deck/simulator.py +++ b/api/src/opentrons/drivers/temp_deck/simulator.py @@ -11,10 +11,13 @@ class SimulatingDriver(AbstractTempDeckDriver): - def __init__(self, sim_model: Optional[str] = None): + def __init__( + self, sim_model: Optional[str] = None, serial_number: Optional[str] = None + ): self._temp = Temperature(target=None, current=0) self._port: Optional[str] = None self._model = TEMP_DECK_MODELS[sim_model] if sim_model else "temp_deck_v1.1" + self._serial_number = serial_number @ensure_yield async def set_temperature(self, celsius: float) -> None: @@ -48,7 +51,7 @@ async def enter_programming_mode(self) -> None: @ensure_yield async def get_device_info(self) -> Dict[str, str]: return { - "serial": "dummySerialTD", + "serial": self._serial_number if self._serial_number else "dummySerialTD", "model": self._model, "version": "dummyVersionTD", } diff --git a/api/src/opentrons/drivers/thermocycler/simulator.py b/api/src/opentrons/drivers/thermocycler/simulator.py index 4a92bb12587..302391a988d 100644 --- a/api/src/opentrons/drivers/thermocycler/simulator.py +++ b/api/src/opentrons/drivers/thermocycler/simulator.py @@ -10,7 +10,9 @@ class SimulatingDriver(AbstractThermocyclerDriver): DEFAULT_TEMP = 23 - def __init__(self, model: Optional[str] = None) -> None: + def __init__( + self, model: Optional[str] = None, serial_number: Optional[str] = None + ) -> None: self._ramp_rate: Optional[float] = None self._lid_status = ThermocyclerLidStatus.OPEN self._lid_temperature = Temperature(current=self.DEFAULT_TEMP, target=None) @@ -18,6 +20,7 @@ def __init__(self, model: Optional[str] = None) -> None: current=self.DEFAULT_TEMP, target=None, hold=None ) self._model = model if model else "thermocyclerModuleV1" + self._serial_number = serial_number def model(self) -> str: return self._model @@ -103,7 +106,7 @@ async def deactivate_all(self) -> None: @ensure_yield async def get_device_info(self) -> Dict[str, str]: return { - "serial": "dummySerialTC", + "serial": self._serial_number if self._serial_number else "dummySerialTC", "model": "dummyModelTC", "version": "dummyVersionTC", } diff --git a/api/src/opentrons/hardware_control/api.py b/api/src/opentrons/hardware_control/api.py index 4b62eba7e3a..de194039618 100644 --- a/api/src/opentrons/hardware_control/api.py +++ b/api/src/opentrons/hardware_control/api.py @@ -255,7 +255,7 @@ async def build_hardware_simulator( attached_instruments: Optional[ Dict[top_types.Mount, Dict[str, Optional[str]]] ] = None, - attached_modules: Optional[List[str]] = None, + attached_modules: Optional[Dict[str, List[str]]] = None, config: Optional[Union[RobotConfig, OT3Config]] = None, loop: Optional[asyncio.AbstractEventLoop] = None, strict_attached_instruments: bool = True, @@ -271,7 +271,7 @@ async def build_hardware_simulator( attached_instruments = {} if None is attached_modules: - attached_modules = [] + attached_modules = {} checked_loop = use_or_initialize_loop(loop) if isinstance(config, RobotConfig): diff --git a/api/src/opentrons/hardware_control/backends/ot3simulator.py b/api/src/opentrons/hardware_control/backends/ot3simulator.py index 07eaaadda00..f57f02e8c51 100644 --- a/api/src/opentrons/hardware_control/backends/ot3simulator.py +++ b/api/src/opentrons/hardware_control/backends/ot3simulator.py @@ -100,7 +100,7 @@ class OT3Simulator(FlexBackend): async def build( cls, attached_instruments: Dict[OT3Mount, Dict[str, Optional[str]]], - attached_modules: List[str], + attached_modules: Dict[str, List[str]], config: OT3Config, loop: asyncio.AbstractEventLoop, strict_attached_instruments: bool = True, @@ -126,7 +126,7 @@ async def build( def __init__( self, attached_instruments: Dict[OT3Mount, Dict[str, Optional[str]]], - attached_modules: List[str], + attached_modules: Dict[str, List[str]], config: OT3Config, loop: asyncio.AbstractEventLoop, strict_attached_instruments: bool = True, @@ -588,10 +588,16 @@ async def increase_z_l_hold_current(self) -> AsyncIterator[None]: @ensure_yield async def watch(self, loop: asyncio.AbstractEventLoop) -> None: - 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) - ] + new_mods_at_ports = [] + for mod, serials in self._stubbed_attached_modules.items(): + for serial in serials: + new_mods_at_ports.append( + modules.SimulatingModuleAtPort( + port=f"/dev/ot_module_sim_{mod}{str(serial)}", + name=mod, + serial_number=serial, + ) + ) await self.module_controls.register_modules(new_mods_at_ports=new_mods_at_ports) @property diff --git a/api/src/opentrons/hardware_control/backends/simulator.py b/api/src/opentrons/hardware_control/backends/simulator.py index d8bca2db353..4066afa4bb5 100644 --- a/api/src/opentrons/hardware_control/backends/simulator.py +++ b/api/src/opentrons/hardware_control/backends/simulator.py @@ -49,7 +49,7 @@ class Simulator: async def build( cls, attached_instruments: Dict[types.Mount, Dict[str, Optional[str]]], - attached_modules: List[str], + attached_modules: Dict[str, List[str]], config: RobotConfig, loop: asyncio.AbstractEventLoop, strict_attached_instruments: bool = True, @@ -105,7 +105,7 @@ async def build( def __init__( self, attached_instruments: Dict[types.Mount, Dict[str, Optional[str]]], - attached_modules: List[str], + attached_modules: Dict[str, List[str]], config: RobotConfig, loop: asyncio.AbstractEventLoop, gpio_chardev: GPIODriverLike, @@ -332,10 +332,16 @@ def set_active_current(self, axis_currents: Dict[Axis, float]) -> None: @ensure_yield async def watch(self) -> None: - 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) - ] + new_mods_at_ports = [] + for mod, serials in self._stubbed_attached_modules.items(): + for serial in serials: + new_mods_at_ports.append( + modules.SimulatingModuleAtPort( + port=f"/dev/ot_module_sim_{mod}{str(serial)}", + name=mod, + serial_number=serial, + ) + ) await self.module_controls.register_modules(new_mods_at_ports=new_mods_at_ports) @contextmanager diff --git a/api/src/opentrons/hardware_control/module_control.py b/api/src/opentrons/hardware_control/module_control.py index ee64b82dd9e..1d32731d026 100644 --- a/api/src/opentrons/hardware_control/module_control.py +++ b/api/src/opentrons/hardware_control/module_control.py @@ -15,6 +15,8 @@ save_module_calibration_offset, ) from opentrons.hardware_control.modules.types import ModuleType +from opentrons.hardware_control.modules import SimulatingModuleAtPort + from opentrons.types import Point from .types import AionotifyEvent, BoardRevision, OT3Mount from . import modules @@ -84,6 +86,7 @@ async def build_module( usb_port: types.USBPort, type: modules.ModuleType, sim_model: Optional[str] = None, + sim_serial_number: Optional[str] = None, ) -> modules.AbstractModule: return await modules.build( port=port, @@ -93,10 +96,14 @@ async def build_module( hw_control_loop=self._api.loop, execution_manager=self._api._execution_manager, sim_model=sim_model, + sim_serial_number=sim_serial_number, ) async def unregister_modules( - self, mods_at_ports: List[modules.ModuleAtPort] + self, + mods_at_ports: Union[ + List[modules.ModuleAtPort], List[modules.SimulatingModuleAtPort] + ], ) -> None: """ De-register Modules. @@ -126,7 +133,9 @@ async def unregister_modules( async def register_modules( self, - new_mods_at_ports: Optional[List[modules.ModuleAtPort]] = None, + new_mods_at_ports: Optional[ + Union[List[modules.ModuleAtPort], List[modules.SimulatingModuleAtPort]] + ] = None, removed_mods_at_ports: Optional[List[modules.ModuleAtPort]] = None, ) -> None: """ @@ -152,6 +161,9 @@ async def register_modules( port=mod.port, usb_port=mod.usb_port, type=modules.MODULE_TYPE_BY_NAME[mod.name], + sim_serial_number=mod.serial_number + if isinstance(mod, SimulatingModuleAtPort) + else None, ) self._available_modules.append(new_instance) log.info( diff --git a/api/src/opentrons/hardware_control/modules/__init__.py b/api/src/opentrons/hardware_control/modules/__init__.py index 4a8208dce49..dd8c531bdb1 100644 --- a/api/src/opentrons/hardware_control/modules/__init__.py +++ b/api/src/opentrons/hardware_control/modules/__init__.py @@ -11,6 +11,7 @@ BundledFirmware, UpdateError, ModuleAtPort, + SimulatingModuleAtPort, ModuleType, ModuleModel, TemperatureStatus, @@ -33,6 +34,7 @@ "BundledFirmware", "UpdateError", "ModuleAtPort", + "SimulatingModuleAtPort", "HeaterShaker", "ModuleType", "ModuleModel", diff --git a/api/src/opentrons/hardware_control/modules/heater_shaker.py b/api/src/opentrons/hardware_control/modules/heater_shaker.py index d4a8fb11d94..09ac06ea5f2 100644 --- a/api/src/opentrons/hardware_control/modules/heater_shaker.py +++ b/api/src/opentrons/hardware_control/modules/heater_shaker.py @@ -49,6 +49,7 @@ async def build( poll_interval_seconds: Optional[float] = None, simulating: bool = False, sim_model: Optional[str] = None, + sim_serial_number: Optional[str] = None, ) -> "HeaterShaker": """ Build a HeaterShaker @@ -71,7 +72,7 @@ async def build( driver = await HeaterShakerDriver.create(port=port, loop=hw_control_loop) poll_interval_seconds = poll_interval_seconds or POLL_PERIOD else: - driver = SimulatingDriver() + driver = SimulatingDriver(serial_number=sim_serial_number) poll_interval_seconds = poll_interval_seconds or SIMULATING_POLL_PERIOD reader = HeaterShakerReader(driver=driver) diff --git a/api/src/opentrons/hardware_control/modules/magdeck.py b/api/src/opentrons/hardware_control/modules/magdeck.py index e195716882a..07c0f2ffb5c 100644 --- a/api/src/opentrons/hardware_control/modules/magdeck.py +++ b/api/src/opentrons/hardware_control/modules/magdeck.py @@ -53,13 +53,16 @@ async def build( poll_interval_seconds: Optional[float] = None, simulating: bool = False, sim_model: Optional[str] = None, + sim_serial_number: Optional[str] = None, ) -> "MagDeck": """Factory function.""" driver: AbstractMagDeckDriver if not simulating: driver = await MagDeckDriver.create(port=port, loop=hw_control_loop) else: - driver = SimulatingDriver(sim_model=sim_model) + driver = SimulatingDriver( + sim_model=sim_model, serial_number=sim_serial_number + ) mod = cls( port=port, diff --git a/api/src/opentrons/hardware_control/modules/mod_abc.py b/api/src/opentrons/hardware_control/modules/mod_abc.py index 48d7f79e4b2..c6ea41437eb 100644 --- a/api/src/opentrons/hardware_control/modules/mod_abc.py +++ b/api/src/opentrons/hardware_control/modules/mod_abc.py @@ -32,6 +32,7 @@ async def build( poll_interval_seconds: Optional[float] = None, simulating: bool = False, sim_model: Optional[str] = None, + sim_serial_number: Optional[str] = None, ) -> "AbstractModule": """Modules should always be created using this factory. diff --git a/api/src/opentrons/hardware_control/modules/tempdeck.py b/api/src/opentrons/hardware_control/modules/tempdeck.py index 261d40ea026..afcc4d64636 100644 --- a/api/src/opentrons/hardware_control/modules/tempdeck.py +++ b/api/src/opentrons/hardware_control/modules/tempdeck.py @@ -39,6 +39,7 @@ async def build( poll_interval_seconds: Optional[float] = None, simulating: bool = False, sim_model: Optional[str] = None, + sim_serial_number: Optional[str] = None, ) -> "TempDeck": """ Build a TempDeck @@ -60,7 +61,9 @@ async def build( driver = await TempDeckDriver.create(port=port, loop=hw_control_loop) poll_interval_seconds = poll_interval_seconds or TEMP_POLL_INTERVAL_SECS else: - driver = SimulatingDriver(sim_model=sim_model) + driver = SimulatingDriver( + sim_model=sim_model, serial_number=sim_serial_number + ) poll_interval_seconds = poll_interval_seconds or SIM_TEMP_POLL_INTERVAL_SECS reader = TempDeckReader(driver=driver) diff --git a/api/src/opentrons/hardware_control/modules/thermocycler.py b/api/src/opentrons/hardware_control/modules/thermocycler.py index fe333d37849..f93cd61ded9 100644 --- a/api/src/opentrons/hardware_control/modules/thermocycler.py +++ b/api/src/opentrons/hardware_control/modules/thermocycler.py @@ -63,6 +63,7 @@ async def build( poll_interval_seconds: Optional[float] = None, simulating: bool = False, sim_model: Optional[str] = None, + sim_serial_number: Optional[str] = None, ) -> "Thermocycler": """ Build and connect to a Thermocycler @@ -87,7 +88,7 @@ async def build( ) poll_interval_seconds = poll_interval_seconds or POLLING_FREQUENCY_SEC else: - driver = SimulatingDriver(model=sim_model) + driver = SimulatingDriver(model=sim_model, serial_number=sim_serial_number) poll_interval_seconds = poll_interval_seconds or SIM_POLLING_FREQUENCY_SEC reader = ThermocyclerReader(driver=driver) diff --git a/api/src/opentrons/hardware_control/modules/types.py b/api/src/opentrons/hardware_control/modules/types.py index 653b0b08e4f..1a87d60d35e 100644 --- a/api/src/opentrons/hardware_control/modules/types.py +++ b/api/src/opentrons/hardware_control/modules/types.py @@ -103,13 +103,18 @@ def module_model_from_string(model_string: str) -> ModuleModel: raise ValueError(f"No such module model {model_string}") -@dataclass +@dataclass(kw_only=True) class ModuleAtPort: port: str name: str usb_port: USBPort = USBPort(name="", port_number=0) +@dataclass(kw_only=True) +class SimulatingModuleAtPort(ModuleAtPort): + serial_number: str + + class BundledFirmware(NamedTuple): """Represents a versioned firmware file, generally bundled into the fs""" diff --git a/api/src/opentrons/hardware_control/modules/utils.py b/api/src/opentrons/hardware_control/modules/utils.py index 56a47f977da..0c213ead6a1 100644 --- a/api/src/opentrons/hardware_control/modules/utils.py +++ b/api/src/opentrons/hardware_control/modules/utils.py @@ -42,6 +42,7 @@ async def build( hw_control_loop: asyncio.AbstractEventLoop, execution_manager: ExecutionManager, sim_model: Optional[str] = None, + sim_serial_number: Optional[str] = None, ) -> AbstractModule: return await _MODULE_CLS_BY_TYPE[type].build( port=port, @@ -50,6 +51,7 @@ async def build( hw_control_loop=hw_control_loop, execution_manager=execution_manager, sim_model=sim_model, + sim_serial_number=sim_serial_number, ) diff --git a/api/src/opentrons/hardware_control/ot3api.py b/api/src/opentrons/hardware_control/ot3api.py index 36509e7fb8d..ef523410f91 100644 --- a/api/src/opentrons/hardware_control/ot3api.py +++ b/api/src/opentrons/hardware_control/ot3api.py @@ -412,7 +412,7 @@ async def build_hardware_simulator( Dict[OT3Mount, Dict[str, Optional[str]]], Dict[top_types.Mount, Dict[str, Optional[str]]], ] = None, - attached_modules: Optional[List[str]] = None, + attached_modules: Optional[Dict[str, List[str]]] = None, config: Union[RobotConfig, OT3Config, None] = None, loop: Optional[asyncio.AbstractEventLoop] = None, strict_attached_instruments: bool = True, @@ -426,7 +426,7 @@ async def build_hardware_simulator( if feature_flags is None: feature_flags = HardwareFeatureFlags() - checked_modules = attached_modules or [] + checked_modules = attached_modules or {} checked_loop = use_or_initialize_loop(loop) if not isinstance(config, OT3Config): diff --git a/api/src/opentrons/hardware_control/simulator_setup.py b/api/src/opentrons/hardware_control/simulator_setup.py index 7c00821d293..25fa17d36a1 100644 --- a/api/src/opentrons/hardware_control/simulator_setup.py +++ b/api/src/opentrons/hardware_control/simulator_setup.py @@ -21,13 +21,19 @@ class ModuleCall: kwargs: Dict[str, Any] = field(default_factory=dict) +@dataclass(frozen=True) +class ModuleItem: + serial_number: str + calls: List[ModuleCall] = field(default_factory=list) + + @dataclass(frozen=True) class OT2SimulatorSetup: machine: Literal["OT-2 Standard"] = "OT-2 Standard" attached_instruments: Dict[Mount, Dict[str, Optional[str]]] = field( default_factory=dict ) - attached_modules: Dict[str, List[ModuleCall]] = field(default_factory=dict) + attached_modules: Dict[str, List[ModuleItem]] = field(default_factory=dict) config: Optional[RobotConfig] = None strict_attached_instruments: bool = True @@ -38,7 +44,7 @@ class OT3SimulatorSetup: attached_instruments: Dict[OT3Mount, Dict[str, Optional[str]]] = field( default_factory=dict ) - attached_modules: Dict[str, List[ModuleCall]] = field(default_factory=dict) + attached_modules: Dict[str, List[ModuleItem]] = field(default_factory=dict) config: Optional[OT3Config] = None strict_attached_instruments: bool = True @@ -52,7 +58,10 @@ async def _simulator_for_setup( if setup.machine == "OT-2 Standard": return await API.build_hardware_simulator( attached_instruments=setup.attached_instruments, - attached_modules=list(setup.attached_modules.keys()), + attached_modules={ + k: [m.serial_number for m in v] + for k, v in setup.attached_modules.items() + }, config=setup.config, strict_attached_instruments=setup.strict_attached_instruments, loop=loop, @@ -63,7 +72,10 @@ async def _simulator_for_setup( return await OT3API.build_hardware_simulator( attached_instruments=setup.attached_instruments, - attached_modules=list(setup.attached_modules.keys()), + attached_modules={ + k: [m.serial_number for m in v] + for k, v in setup.attached_modules.items() + }, config=setup.config, strict_attached_instruments=setup.strict_attached_instruments, loop=loop, @@ -77,10 +89,12 @@ async def create_simulator( """Create a simulator""" simulator = await _simulator_for_setup(setup, loop) for attached_module in simulator.attached_modules: - calls = setup.attached_modules[attached_module.name()] - for call in calls: - f = getattr(attached_module, call.function_name) - await f(*call.args, **call.kwargs) + modules = setup.attached_modules[attached_module.name()] + for module in modules: + if module.serial_number == attached_module.device_info.get("serial"): + for call in module.calls: + f = getattr(attached_module, call.function_name) + await f(*call.args, **call.kwargs) return simulator @@ -99,7 +113,10 @@ def _thread_manager_for_setup( return ThreadManager( API.build_hardware_simulator, attached_instruments=setup.attached_instruments, - attached_modules=list(setup.attached_modules.keys()), + attached_modules={ + k: [m.serial_number for m in v] + for k, v in setup.attached_modules.items() + }, config=setup.config, strict_attached_instruments=setup.strict_attached_instruments, feature_flags=HardwareFeatureFlags.build_from_ff(), @@ -110,7 +127,10 @@ def _thread_manager_for_setup( return ThreadManager( OT3API.build_hardware_simulator, attached_instruments=setup.attached_instruments, - attached_modules=list(setup.attached_modules.keys()), + attached_modules={ + k: [m.serial_number for m in v] + for k, v in setup.attached_modules.items() + }, config=setup.config, strict_attached_instruments=setup.strict_attached_instruments, feature_flags=HardwareFeatureFlags.build_from_ff(), @@ -125,10 +145,11 @@ async def create_simulator_thread_manager( await thread_manager.managed_thread_ready_async() for attached_module in thread_manager.wrapped().attached_modules: - calls = setup.attached_modules[attached_module.name()] - for call in calls: - f = getattr(attached_module, call.function_name) - await f(*call.args, **call.kwargs) + modules = setup.attached_modules[attached_module.name()] + for module in modules: + for call in module.calls: + f = getattr(attached_module, call.function_name) + await f(*call.args, **call.kwargs) return thread_manager @@ -188,7 +209,18 @@ def _prepare_for_simulator_setup(key: str, value: Dict[str, Any]) -> Any: if key == "config" and value: return robot_configs.build_config_ot2(value) if key == "attached_modules" and value: - return {k: [ModuleCall(**data) for data in v] for (k, v) in value.items()} + attached_modules: Dict[str, List[ModuleItem]] = {} + for key, item in value.items(): + for obj in item: + attached_modules.setdefault(key, []).append( + ModuleItem( + serial_number=obj["serial_number"], + calls=[ModuleCall(**data) for data in obj["calls"]], + ) + ) + + return attached_modules + return value @@ -198,5 +230,15 @@ def _prepare_for_ot3_simulator_setup(key: str, value: Dict[str, Any]) -> Any: if key == "config" and value: return robot_configs.build_config_ot3(value) if key == "attached_modules" and value: - return {k: [ModuleCall(**data) for data in v] for (k, v) in value.items()} + attached_modules: Dict[str, List[ModuleItem]] = {} + for key, item in value.items(): + for obj in item: + attached_modules.setdefault(key, []).append( + ModuleItem( + serial_number=obj["serial_number"], + calls=[ModuleCall(**data) for data in obj["calls"]], + ) + ) + + return attached_modules return value diff --git a/api/tests/opentrons/hardware_control/test_module_control.py b/api/tests/opentrons/hardware_control/test_module_control.py index eed809bdb55..36fd6cb1793 100644 --- a/api/tests/opentrons/hardware_control/test_module_control.py +++ b/api/tests/opentrons/hardware_control/test_module_control.py @@ -1,13 +1,17 @@ """Tests for opentrons.hardware_control.module_control.""" import pytest from decoy import Decoy, matchers -from typing import Awaitable, Callable, cast +from typing import Awaitable, Callable, cast, Union, List from opentrons.drivers.rpi_drivers.types import USBPort from opentrons.drivers.rpi_drivers.interfaces import USBDriverInterface from opentrons.hardware_control import API as HardwareAPI from opentrons.hardware_control.modules import AbstractModule -from opentrons.hardware_control.modules.types import ModuleAtPort, ModuleType +from opentrons.hardware_control.modules.types import ( + ModuleAtPort, + ModuleType, + SimulatingModuleAtPort, +) from opentrons.hardware_control.module_control import AttachedModulesControl @@ -55,15 +59,28 @@ def subject( return modules_control +@pytest.mark.parametrize( + "module_at_port_input", + [ + ([ModuleAtPort(port="/dev/foo", name="bar")]), + ( + [ + SimulatingModuleAtPort( + port="/dev/foo", name="bar", serial_number="test-123" + ) + ] + ), + ], +) async def test_register_modules( decoy: Decoy, usb_bus: USBDriverInterface, build_module: Callable[..., Awaitable[AbstractModule]], hardware_api: HardwareAPI, subject: AttachedModulesControl, + module_at_port_input: Union[List[ModuleAtPort], List[SimulatingModuleAtPort]], ) -> None: """It should register attached modules.""" - new_mods_at_ports = [ModuleAtPort(port="/dev/foo", name="bar")] actual_ports = [ ModuleAtPort( port="/dev/foo", @@ -75,16 +92,19 @@ async def test_register_modules( module = decoy.mock(cls=AbstractModule) decoy.when(module.usb_port).then_return(USBPort(name="baz", port_number=0)) - decoy.when(usb_bus.match_virtual_ports(new_mods_at_ports)).then_return(actual_ports) + decoy.when(usb_bus.match_virtual_ports(module_at_port_input)).then_return( + actual_ports + ) decoy.when( await build_module( port="/dev/foo", usb_port=USBPort(name="baz", port_number=0), type=ModuleType.TEMPERATURE, + sim_serial_number=None, ) ).then_return(module) - await subject.register_modules(new_mods_at_ports=new_mods_at_ports) + await subject.register_modules(new_mods_at_ports=module_at_port_input) result = subject.available_modules assert result == [module] @@ -130,6 +150,7 @@ async def test_register_modules_sort( usb_port=mod.usb_port, port=matchers.Anything(), type=matchers.Anything(), + sim_serial_number=None, ) ).then_return(mod) diff --git a/api/tests/opentrons/hardware_control/test_modules.py b/api/tests/opentrons/hardware_control/test_modules.py index 49e6ba4b766..238663bf5a2 100644 --- a/api/tests/opentrons/hardware_control/test_modules.py +++ b/api/tests/opentrons/hardware_control/test_modules.py @@ -28,7 +28,12 @@ async def test_get_modules_simulating(): import opentrons.hardware_control as hardware_control - mods = ["tempdeck", "magdeck", "thermocycler", "heatershaker"] + mods = { + "tempdeck": ["111"], + "magdeck": ["222"], + "thermocycler": ["333"], + "heatershaker": ["444"], + } api = await hardware_control.API.build_hardware_simulator(attached_modules=mods) await asyncio.sleep(0.05) from_api = api.attached_modules @@ -40,7 +45,7 @@ async def test_get_modules_simulating(): async def test_module_caching(): import opentrons.hardware_control as hardware_control - mod_names = ["tempdeck"] + mod_names = {"tempdeck": ["111"]} api = await hardware_control.API.build_hardware_simulator( attached_modules=mod_names ) @@ -59,10 +64,11 @@ async def test_module_caching(): assert with_magdeck[0] is found_mods[0] await api._backend.module_controls.register_modules( removed_mods_at_ports=[ - ModuleAtPort(port="/dev/ot_module_sim_tempdeck0", name="tempdeck") + ModuleAtPort(port="/dev/ot_module_sim_tempdeck111", name="tempdeck") ] ) only_magdeck = api.attached_modules.copy() + assert only_magdeck[0] is with_magdeck[1] # Check that two modules of the same kind on different ports are @@ -94,7 +100,7 @@ async def test_create_simulating_module( """It should create simulating module instance for specified module.""" import opentrons.hardware_control as hardware_control - api = await hardware_control.API.build_hardware_simulator(attached_modules=[]) + api = await hardware_control.API.build_hardware_simulator(attached_modules={}) await asyncio.sleep(0.05) simulating_module = await api.create_simulating_module(module_model) @@ -343,7 +349,13 @@ async def test_get_bundled_fw(monkeypatch, tmpdir): from opentrons.hardware_control import API - mods = ["tempdeck", "magdeck", "thermocycler", "heatershaker"] + mods = { + "tempdeck": ["111"], + "magdeck": ["222"], + "thermocycler": ["333"], + "heatershaker": ["444"], + } + api = await API.build_hardware_simulator(attached_modules=mods) await asyncio.sleep(0.05) diff --git a/api/tests/opentrons/hardware_control/test_simulator_setup.py b/api/tests/opentrons/hardware_control/test_simulator_setup.py index 422375f1bf6..2507a9969b3 100644 --- a/api/tests/opentrons/hardware_control/test_simulator_setup.py +++ b/api/tests/opentrons/hardware_control/test_simulator_setup.py @@ -56,7 +56,16 @@ async def test_with_magdeck(setup_klass: Type[simulator_setup.SimulatorSetup]) - """It should work to build a magdeck.""" setup = setup_klass( attached_modules={ - "magdeck": [simulator_setup.ModuleCall("engage", kwargs={"height": 3})] + "magdeck": [ + simulator_setup.ModuleItem( + serial_number="123", + calls=[simulator_setup.ModuleCall("engage", kwargs={"height": 3})], + ), + simulator_setup.ModuleItem( + serial_number="1234", + calls=[simulator_setup.ModuleCall("engage", kwargs={"height": 5})], + ), + ] } ) simulator = await simulator_setup.create_simulator(setup) @@ -66,6 +75,12 @@ async def test_with_magdeck(setup_klass: Type[simulator_setup.SimulatorSetup]) - "data": {"engaged": True, "height": 3}, "status": "engaged", } + assert simulator.attached_modules[0].device_info["serial"] == "123" + assert simulator.attached_modules[1].live_data == { + "data": {"engaged": True, "height": 5}, + "status": "engaged", + } + assert simulator.attached_modules[1].device_info["serial"] == "1234" async def test_with_thermocycler( @@ -75,14 +90,19 @@ async def test_with_thermocycler( setup = setup_klass( attached_modules={ "thermocycler": [ - simulator_setup.ModuleCall( - "set_temperature", - kwargs={ - "temperature": 3, - "hold_time_seconds": 1, - "hold_time_minutes": 2, - "volume": 5, - }, + simulator_setup.ModuleItem( + serial_number="123", + calls=[ + simulator_setup.ModuleCall( + "set_temperature", + kwargs={ + "temperature": 3, + "hold_time_seconds": 1, + "hold_time_minutes": 2, + "volume": 5, + }, + ) + ], ) ] } @@ -107,6 +127,7 @@ async def test_with_thermocycler( }, "status": "holding at target", } + assert simulator.attached_modules[0].device_info["serial"] == "123" async def test_with_tempdeck(setup_klass: Type[simulator_setup.SimulatorSetup]) -> None: @@ -114,12 +135,17 @@ async def test_with_tempdeck(setup_klass: Type[simulator_setup.SimulatorSetup]) setup = setup_klass( attached_modules={ "tempdeck": [ - simulator_setup.ModuleCall( - "start_set_temperature", kwargs={"celsius": 23} - ), - simulator_setup.ModuleCall( - "await_temperature", kwargs={"awaiting_temperature": None} - ), + simulator_setup.ModuleItem( + serial_number="123", + calls=[ + simulator_setup.ModuleCall( + "start_set_temperature", kwargs={"celsius": 23} + ), + simulator_setup.ModuleCall( + "await_temperature", kwargs={"awaiting_temperature": None} + ), + ], + ) ] } ) @@ -130,6 +156,7 @@ async def test_with_tempdeck(setup_klass: Type[simulator_setup.SimulatorSetup]) "data": {"currentTemp": 23, "targetTemp": 23}, "status": "holding at target", } + assert simulator.attached_modules[0].device_info["serial"] == "123" def test_persistence_ot2(tmpdir: str) -> None: @@ -139,10 +166,24 @@ def test_persistence_ot2(tmpdir: str) -> None: Mount.RIGHT: {"id": "some id"}, }, attached_modules={ - "magdeck": [simulator_setup.ModuleCall("engage", kwargs={"height": 3})], + "magdeck": [ + simulator_setup.ModuleItem( + serial_number="111", + calls=[simulator_setup.ModuleCall("engage", kwargs={"height": 3})], + ) + ], "tempdeck": [ - simulator_setup.ModuleCall("set_temperature", kwargs={"celsius": 23}), - simulator_setup.ModuleCall("set_temperature", kwargs={"celsius": 24}), + simulator_setup.ModuleItem( + serial_number="111", + calls=[ + simulator_setup.ModuleCall( + "set_temperature", kwargs={"celsius": 23} + ), + simulator_setup.ModuleCall( + "set_temperature", kwargs={"celsius": 24} + ), + ], + ) ], }, config=robot_configs.build_config_ot2({}), @@ -162,10 +203,44 @@ def test_persistence_ot3(tmpdir: str) -> None: OT3Mount.GRIPPER: {"id": "some-other-id"}, }, attached_modules={ - "magdeck": [simulator_setup.ModuleCall("engage", kwargs={"height": 3})], + "magdeck": [ + simulator_setup.ModuleItem( + serial_number="mag-1", + calls=[ + simulator_setup.ModuleCall( + function_name="engage", + kwargs={"height": 3}, + ) + ], + ) + ], "tempdeck": [ - simulator_setup.ModuleCall("set_temperature", kwargs={"celsius": 23}), - simulator_setup.ModuleCall("set_temperature", kwargs={"celsius": 24}), + simulator_setup.ModuleItem( + serial_number="temp-1", + calls=[ + simulator_setup.ModuleCall( + function_name="set_temperature", + kwargs={"celsius": 23}, + ), + simulator_setup.ModuleCall( + function_name="set_temperature", + kwargs={"celsius": 24}, + ), + ], + ), + simulator_setup.ModuleItem( + serial_number="temp-2", + calls=[ + simulator_setup.ModuleCall( + function_name="set_temperature", + kwargs={"celsius": 23}, + ), + simulator_setup.ModuleCall( + function_name="set_temperature", + kwargs={"celsius": 24}, + ), + ], + ), ], }, config=robot_configs.build_config_ot3({}), diff --git a/api/tests/opentrons/hardware_control/test_thread_manager.py b/api/tests/opentrons/hardware_control/test_thread_manager.py index fe3f53309ad..193740b4d75 100644 --- a/api/tests/opentrons/hardware_control/test_thread_manager.py +++ b/api/tests/opentrons/hardware_control/test_thread_manager.py @@ -28,7 +28,7 @@ def test_build_fail_raises_exception(): def test_module_cache_add_entry(): """Test that _cached_modules updates correctly.""" - mod_names = ["tempdeck"] + mod_names = {"tempdeck": ["111"]} thread_manager = ThreadManager( API.build_hardware_simulator, attached_modules=mod_names ) @@ -49,7 +49,7 @@ def test_module_cache_add_entry(): async def test_module_cache_remove_entry(): """Test that module entry gets removed from cache when module detaches.""" - mod_names = ["tempdeck", "magdeck"] + mod_names = {"tempdeck": ["111"], "magdeck": ["222"]} thread_manager = ThreadManager( API.build_hardware_simulator, attached_modules=mod_names ) @@ -63,7 +63,7 @@ async def test_module_cache_remove_entry(): future = asyncio.run_coroutine_threadsafe( thread_manager._backend.module_controls.register_modules( removed_mods_at_ports=[ - ModuleAtPort(port="/dev/ot_module_sim_tempdeck0", name="tempdeck") + ModuleAtPort(port="/dev/ot_module_sim_tempdeck111", name="tempdeck") ] ), loop, diff --git a/api/tests/opentrons/protocol_api_old/test_context.py b/api/tests/opentrons/protocol_api_old/test_context.py index bb8b8f6c7ca..db45d3af6c6 100644 --- a/api/tests/opentrons/protocol_api_old/test_context.py +++ b/api/tests/opentrons/protocol_api_old/test_context.py @@ -959,7 +959,7 @@ def test_order_of_module_load(): import opentrons.hardware_control as hardware_control import opentrons.protocol_api as protocol_api - mods = ["tempdeck", "thermocycler", "tempdeck"] + mods = {"tempdeck": ["111", "333"], "thermocycler": ["222"]} thread_manager = hardware_control.ThreadManager( hardware_control.API.build_hardware_simulator, attached_modules=mods ) @@ -967,7 +967,7 @@ def test_order_of_module_load(): attached_modules = fake_hardware.attached_modules hw_temp1 = attached_modules[0] - hw_temp2 = attached_modules[2] + hw_temp2 = attached_modules[1] ctx1 = protocol_api.create_protocol_context( api_version=APIVersion(2, 13), diff --git a/robot-server/simulators/test-flex.json b/robot-server/simulators/test-flex.json index f7044a53ca7..d7fc860c662 100644 --- a/robot-server/simulators/test-flex.json +++ b/robot-server/simulators/test-flex.json @@ -14,44 +14,81 @@ "attached_modules": { "thermocycler": [ { - "function_name": "set_temperature", - "kwargs": { - "temperature": 3, - "hold_time_seconds": 1, - "hold_time_minutes": 2, - "ramp_rate": 4, - "volume": 5 - } - }, + "serial_number": "therm-123", + "calls": [ + { + "function_name": "set_temperature", + "kwargs": { + "temperature": 3, + "hold_time_seconds": 1, + "hold_time_minutes": 2, + "ramp_rate": 4, + "volume": 5 + } + }, + { + "function_name": "set_lid_temperature", + "kwargs": { + "temperature": 4 + } + } + ] + } + ], + "heatershaker": [ { - "function_name": "set_lid_temperature", - "kwargs": { - "temperature": 4 - } + "serial_number": "hs-123", + "calls": [] } ], - "heatershaker": [], "tempdeck": [ { - "function_name": "start_set_temperature", - "kwargs": { - "celsius": 3 - } + "serial_number": "temp-123", + "calls": [ + { + "function_name": "start_set_temperature", + "kwargs": { + "celsius": 3 + } + }, + { + "function_name": "await_temperature", + "kwargs": { + "awaiting_temperature": null + } + } + ] }, { - "function_name": "await_temperature", - "kwargs": { - "awaiting_temperature": null - } + "serial_number": "temp-1234", + "calls": [ + { + "function_name": "start_set_temperature", + "kwargs": { + "celsius": 3 + } + }, + { + "function_name": "await_temperature", + "kwargs": { + "awaiting_temperature": null + } + } + ] } ], "magdeck": [ { - "function_name": "engage", - "kwargs": { - "height": 4 - } + "serial_number": "mag-123", + "calls": [ + { + "function_name": "engage", + "kwargs": { + "height": 4 + } + } + ] } ] } -} +} \ No newline at end of file diff --git a/robot-server/simulators/test.json b/robot-server/simulators/test.json index 0e23a2e8351..c7ca49e9040 100644 --- a/robot-server/simulators/test.json +++ b/robot-server/simulators/test.json @@ -12,44 +12,75 @@ "attached_modules": { "thermocycler": [ { - "function_name": "set_temperature", - "kwargs": { - "temperature": 3, - "hold_time_seconds": 1, - "hold_time_minutes": 2, - "ramp_rate": 4, - "volume": 5 - } - }, + "serial_number": "therm-123", + "calls": [ + { + "function_name": "set_temperature", + "kwargs": { + "temperature": 3, + "hold_time_seconds": 1, + "hold_time_minutes": 2, + "ramp_rate": 4, + "volume": 5 + } + }, + { + "function_name": "set_lid_temperature", + "kwargs": { + "temperature": 4 + } + } + ] + } + ], + "heatershaker": [ { - "function_name": "set_lid_temperature", - "kwargs": { - "temperature": 4 - } + "serial_number": "hs-123", + "calls": [] } ], - "heatershaker": [], "tempdeck": [ { - "function_name": "start_set_temperature", - "kwargs": { - "celsius": 3 - } - }, - { - "function_name": "await_temperature", - "kwargs": { - "awaiting_temperature": null - } + "serial_number": "temp-123", + "calls": [ + { + "function_name": "start_set_temperature", + "kwargs": { + "celsius": 3 + } + }, + { + "function_name": "await_temperature", + "kwargs": { + "awaiting_temperature": null + } + } + ] } ], "magdeck": [ { - "function_name": "engage", - "kwargs": { - "height": 4 - } + "serial_number": "mag-123", + "calls": [ + { + "function_name": "engage", + "kwargs": { + "height": 4 + } + } + ] + }, + { + "serial_number": "mag-1234", + "calls": [ + { + "function_name": "engage", + "kwargs": { + "height": 4 + } + } + ] } ] } -} +} \ No newline at end of file diff --git a/robot-server/tests/integration/test_modules.tavern.yaml b/robot-server/tests/integration/test_modules.tavern.yaml index 93930465eb8..48220193df7 100644 --- a/robot-server/tests/integration/test_modules.tavern.yaml +++ b/robot-server/tests/integration/test_modules.tavern.yaml @@ -85,6 +85,20 @@ stages: data: height: !anyfloat engaged: !anybool + - name: magdeck + displayName: magdeck + moduleModel: magneticModuleV1 + port: !anystr + usbPort: !anydict + serial: !anystr + model: !anystr + revision: !anystr + fwVersion: !anystr + hasAvailableUpdate: !anybool + status: !anystr + data: + height: !anyfloat + engaged: !anybool - name: Get all the modules request: url: '{ot2_server_base_url}/modules' @@ -166,3 +180,19 @@ stages: status: !anystr height: !anyfloat engaged: !anybool + - id: !anystr + serialNumber: !anystr + firmwareVersion: !anystr + hardwareRevision: !anystr + hasAvailableUpdate: !anybool + moduleType: magneticModuleType + moduleModel: magneticModuleV1 + usbPort: + port: !anyint + hub: !anybool + portGroup: !anystr + path: !anystr + data: + status: !anystr + height: !anyfloat + engaged: !anybool