From e3b748c4cbe8a25d07b9fdb459bafb20719650f1 Mon Sep 17 00:00:00 2001 From: tamarzanzouri Date: Thu, 29 Feb 2024 17:01:40 -0500 Subject: [PATCH 01/31] change structure of loadedModules --- .../hardware_control/simulator_setup.py | 16 ++++++++++ .../hardware_control/test_simulator_setup.py | 31 +++++++++++++++++-- 2 files changed, 44 insertions(+), 3 deletions(-) diff --git a/api/src/opentrons/hardware_control/simulator_setup.py b/api/src/opentrons/hardware_control/simulator_setup.py index 7c00821d293..bdadba9396f 100644 --- a/api/src/opentrons/hardware_control/simulator_setup.py +++ b/api/src/opentrons/hardware_control/simulator_setup.py @@ -21,6 +21,13 @@ class ModuleCall: kwargs: Dict[str, Any] = field(default_factory=dict) +# Name and kwargs for a module function +@dataclass(frozen=True) +class ModuleItem: + id: str + items: List[ModuleCall] = field(default_factory=list) + + @dataclass(frozen=True) class OT2SimulatorSetup: machine: Literal["OT-2 Standard"] = "OT-2 Standard" @@ -198,5 +205,14 @@ 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: + # list of items with id, item - loop and set to ModuleCall + for key, item in value.items(): + # print(list(item['items'])) + for obj in item: + for data in obj["items"]: + print(ModuleCall(**data)) + + # items = {k: [ModuleCall(**data) for data in v] for (k, v) in value.items()} + # print(items) return {k: [ModuleCall(**data) for data in v] for (k, v) in value.items()} return value diff --git a/api/tests/opentrons/hardware_control/test_simulator_setup.py b/api/tests/opentrons/hardware_control/test_simulator_setup.py index 422375f1bf6..44bda189d81 100644 --- a/api/tests/opentrons/hardware_control/test_simulator_setup.py +++ b/api/tests/opentrons/hardware_control/test_simulator_setup.py @@ -162,10 +162,35 @@ 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( + id="mag-123", + items=[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( + id="temp-123", + items=[ + simulator_setup.ModuleCall( + "set_temperature", kwargs={"celsius": 23} + ), + simulator_setup.ModuleCall( + "set_temperature", kwargs={"celsius": 24} + ), + ], + ), + simulator_setup.ModuleItem( + id="temp-1234", + items=[ + simulator_setup.ModuleCall( + "set_temperature", kwargs={"celsius": 23} + ), + simulator_setup.ModuleCall( + "set_temperature", kwargs={"celsius": 24} + ), + ], + ), ], }, config=robot_configs.build_config_ot3({}), From 8c9d1fec688c2506cd18b962872e8781c30c232d Mon Sep 17 00:00:00 2001 From: tamarzanzouri Date: Thu, 29 Feb 2024 21:01:06 -0500 Subject: [PATCH 02/31] change id to sn --- api/src/opentrons/hardware_control/simulator_setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/src/opentrons/hardware_control/simulator_setup.py b/api/src/opentrons/hardware_control/simulator_setup.py index bdadba9396f..9cea61839d8 100644 --- a/api/src/opentrons/hardware_control/simulator_setup.py +++ b/api/src/opentrons/hardware_control/simulator_setup.py @@ -24,7 +24,7 @@ class ModuleCall: # Name and kwargs for a module function @dataclass(frozen=True) class ModuleItem: - id: str + serial_number: str items: List[ModuleCall] = field(default_factory=list) From 26d057ea34b87605edfcac5af9d0ef154b3819a1 Mon Sep 17 00:00:00 2001 From: tamarzanzouri Date: Thu, 29 Feb 2024 21:01:26 -0500 Subject: [PATCH 03/31] change id to sn --- robot-server/simulators/test-flex.json | 27 ++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/robot-server/simulators/test-flex.json b/robot-server/simulators/test-flex.json index f7044a53ca7..7859e239e6c 100644 --- a/robot-server/simulators/test-flex.json +++ b/robot-server/simulators/test-flex.json @@ -14,6 +14,8 @@ "attached_modules": { "thermocycler": [ { + "serial_number": "123", + "items": [{ "function_name": "set_temperature", "kwargs": { "temperature": 3, @@ -28,10 +30,30 @@ "kwargs": { "temperature": 4 } + }] } ], "heatershaker": [], "tempdeck": [ + { + "serial_number": "temp-123", + "items": [ + { + "function_name": "start_set_temperature", + "kwargs": { + "celsius": 3 + } + }, + { + "function_name": "await_temperature", + "kwargs": { + "awaiting_temperature": null + } + } + ]}, + { + "serial_number": "temp-1234", + "items": [ { "function_name": "start_set_temperature", "kwargs": { @@ -44,14 +66,19 @@ "awaiting_temperature": null } } + ]} ], "magdeck": [ + { + "serial_number": "temp-123", + "items": [ { "function_name": "engage", "kwargs": { "height": 4 } } + ]} ] } } From 756cf51ab8ae237925480a007cb6dc549527f790 Mon Sep 17 00:00:00 2001 From: tamarzanzouri Date: Fri, 1 Mar 2024 20:54:53 -0500 Subject: [PATCH 04/31] SimulatingModuleAtPort just started --- api/src/opentrons/hardware_control/module_control.py | 4 +++- api/src/opentrons/hardware_control/modules/__init__.py | 2 ++ api/src/opentrons/hardware_control/modules/types.py | 8 ++++++++ 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/api/src/opentrons/hardware_control/module_control.py b/api/src/opentrons/hardware_control/module_control.py index ee64b82dd9e..41d9860aac7 100644 --- a/api/src/opentrons/hardware_control/module_control.py +++ b/api/src/opentrons/hardware_control/module_control.py @@ -126,7 +126,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: """ 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/types.py b/api/src/opentrons/hardware_control/modules/types.py index 653b0b08e4f..1783c9c2085 100644 --- a/api/src/opentrons/hardware_control/modules/types.py +++ b/api/src/opentrons/hardware_control/modules/types.py @@ -110,6 +110,14 @@ class ModuleAtPort: usb_port: USBPort = USBPort(name="", port_number=0) +@dataclass +class SimulatingModuleAtPort: + serial_numer: str + port: str + name: str + usb_port: USBPort = USBPort(name="", port_number=0) + + class BundledFirmware(NamedTuple): """Represents a versioned firmware file, generally bundled into the fs""" From 9b6ebd4b14480d3e5baa18fc4e227c757302e472 Mon Sep 17 00:00:00 2001 From: tamarzanzouri Date: Fri, 1 Mar 2024 21:33:11 -0500 Subject: [PATCH 05/31] continue - WIP --- .../opentrons/drivers/rpi_drivers/interfaces.py | 9 ++++++--- .../opentrons/drivers/rpi_drivers/usb_simulator.py | 14 ++++++++++---- .../opentrons/hardware_control/modules/types.py | 6 +----- 3 files changed, 17 insertions(+), 12 deletions(-) diff --git a/api/src/opentrons/drivers/rpi_drivers/interfaces.py b/api/src/opentrons/drivers/rpi_drivers/interfaces.py index 3923b250a27..d52288ae2d4 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], + virtual_port: Union[List[ModuleAtPort], List[SimulatingModuleAtPort]], ) -> List[ModuleAtPort]: ... diff --git a/api/src/opentrons/drivers/rpi_drivers/usb_simulator.py b/api/src/opentrons/drivers/rpi_drivers/usb_simulator.py index d3931c00fdd..8dc06207e46 100644 --- a/api/src/opentrons/drivers/rpi_drivers/usb_simulator.py +++ b/api/src/opentrons/drivers/rpi_drivers/usb_simulator.py @@ -4,15 +4,21 @@ 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] + ], # should probably one use simulating + ) -> Union[List[ModuleAtPort], List[SimulatingModuleAtPort]]: return virtual_port diff --git a/api/src/opentrons/hardware_control/modules/types.py b/api/src/opentrons/hardware_control/modules/types.py index 1783c9c2085..7c79f5b0e30 100644 --- a/api/src/opentrons/hardware_control/modules/types.py +++ b/api/src/opentrons/hardware_control/modules/types.py @@ -110,12 +110,8 @@ class ModuleAtPort: usb_port: USBPort = USBPort(name="", port_number=0) -@dataclass -class SimulatingModuleAtPort: +class SimulatingModuleAtPort(ModuleAtPort): serial_numer: str - port: str - name: str - usb_port: USBPort = USBPort(name="", port_number=0) class BundledFirmware(NamedTuple): From 880db71e596b87ed9ef106053876181d9b63542a Mon Sep 17 00:00:00 2001 From: tamarzanzouri Date: Mon, 4 Mar 2024 09:50:37 -0500 Subject: [PATCH 06/31] union of simulating port --- api/src/opentrons/drivers/rpi_drivers/interfaces.py | 2 +- api/src/opentrons/drivers/rpi_drivers/usb.py | 11 +++++++---- .../opentrons/drivers/rpi_drivers/usb_simulator.py | 4 +--- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/api/src/opentrons/drivers/rpi_drivers/interfaces.py b/api/src/opentrons/drivers/rpi_drivers/interfaces.py index d52288ae2d4..f3986ae78d7 100644 --- a/api/src/opentrons/drivers/rpi_drivers/interfaces.py +++ b/api/src/opentrons/drivers/rpi_drivers/interfaces.py @@ -11,5 +11,5 @@ class USBDriverInterface(Protocol): def match_virtual_ports( self, virtual_port: Union[List[ModuleAtPort], List[SimulatingModuleAtPort]], - ) -> List[ModuleAtPort]: + ) -> 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..16786cd68a6 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 diff --git a/api/src/opentrons/drivers/rpi_drivers/usb_simulator.py b/api/src/opentrons/drivers/rpi_drivers/usb_simulator.py index 8dc06207e46..be7cec2e48e 100644 --- a/api/src/opentrons/drivers/rpi_drivers/usb_simulator.py +++ b/api/src/opentrons/drivers/rpi_drivers/usb_simulator.py @@ -17,8 +17,6 @@ class USBBusSimulator(USBDriverInterface): def match_virtual_ports( self, - virtual_port: Union[ - List[ModuleAtPort], List[SimulatingModuleAtPort] - ], # should probably one use simulating + virtual_port: Union[List[ModuleAtPort], List[SimulatingModuleAtPort]], ) -> Union[List[ModuleAtPort], List[SimulatingModuleAtPort]]: return virtual_port From 79e7b9a9b5ef653cea0ccd6053e2fbe664ed9b67 Mon Sep 17 00:00:00 2001 From: tamarzanzouri Date: Mon, 4 Mar 2024 10:38:19 -0500 Subject: [PATCH 07/31] update simulating modules --- api/src/opentrons/drivers/heater_shaker/simulator.py | 7 ++++--- api/src/opentrons/drivers/mag_deck/simulator.py | 7 +++++-- api/src/opentrons/drivers/temp_deck/simulator.py | 7 +++++-- api/src/opentrons/drivers/thermocycler/simulator.py | 7 +++++-- api/src/opentrons/hardware_control/module_control.py | 7 +++++++ .../opentrons/hardware_control/modules/heater_shaker.py | 3 ++- api/src/opentrons/hardware_control/modules/magdeck.py | 3 ++- api/src/opentrons/hardware_control/modules/mod_abc.py | 1 + api/src/opentrons/hardware_control/modules/tempdeck.py | 3 ++- api/src/opentrons/hardware_control/modules/thermocycler.py | 3 ++- api/src/opentrons/hardware_control/modules/types.py | 2 +- api/src/opentrons/hardware_control/modules/utils.py | 2 ++ 12 files changed, 38 insertions(+), 14 deletions(-) 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/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/module_control.py b/api/src/opentrons/hardware_control/module_control.py index 41d9860aac7..254885000ee 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, + serial_number: Optional[str] = None, ) -> modules.AbstractModule: return await modules.build( port=port, @@ -93,6 +96,7 @@ async def build_module( hw_control_loop=self._api.loop, execution_manager=self._api._execution_manager, sim_model=sim_model, + serial_number=serial_number, ) async def unregister_modules( @@ -154,6 +158,9 @@ async def register_modules( port=mod.port, usb_port=mod.usb_port, type=modules.MODULE_TYPE_BY_NAME[mod.name], + 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/heater_shaker.py b/api/src/opentrons/hardware_control/modules/heater_shaker.py index d4a8fb11d94..df23c2e8ef5 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, + 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=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..7f0293485f3 100644 --- a/api/src/opentrons/hardware_control/modules/magdeck.py +++ b/api/src/opentrons/hardware_control/modules/magdeck.py @@ -53,13 +53,14 @@ async def build( poll_interval_seconds: Optional[float] = None, simulating: bool = False, sim_model: Optional[str] = None, + 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=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..72b6b47969e 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, + 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..1fb4ad3a64e 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, + serial_number: Optional[str] = None, ) -> "TempDeck": """ Build a TempDeck @@ -60,7 +61,7 @@ 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=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..04242b8555d 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, + 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=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 7c79f5b0e30..432848ca32b 100644 --- a/api/src/opentrons/hardware_control/modules/types.py +++ b/api/src/opentrons/hardware_control/modules/types.py @@ -111,7 +111,7 @@ class ModuleAtPort: class SimulatingModuleAtPort(ModuleAtPort): - serial_numer: str + serial_number: str class BundledFirmware(NamedTuple): diff --git a/api/src/opentrons/hardware_control/modules/utils.py b/api/src/opentrons/hardware_control/modules/utils.py index 56a47f977da..557c2c4bbf8 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, + 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, + serial_number=serial_number, ) From 64dc4f66289941dfbb8f2c4bccfa74b56e10882b Mon Sep 17 00:00:00 2001 From: tamarzanzouri Date: Mon, 4 Mar 2024 16:07:01 -0500 Subject: [PATCH 08/31] started tests --- .../hardware_control/modules/types.py | 3 ++- .../hardware_control/test_module_control.py | 18 +++++++++++++++--- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/api/src/opentrons/hardware_control/modules/types.py b/api/src/opentrons/hardware_control/modules/types.py index 432848ca32b..1a87d60d35e 100644 --- a/api/src/opentrons/hardware_control/modules/types.py +++ b/api/src/opentrons/hardware_control/modules/types.py @@ -103,13 +103,14 @@ 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 diff --git a/api/tests/opentrons/hardware_control/test_module_control.py b/api/tests/opentrons/hardware_control/test_module_control.py index eed809bdb55..752344954e6 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 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,23 @@ 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[ModuleAtPort, SimulatingModuleAtPort], ) -> None: """It should register attached modules.""" - new_mods_at_ports = [ModuleAtPort(port="/dev/foo", name="bar")] + new_mods_at_ports = [module_at_port_input] actual_ports = [ ModuleAtPort( port="/dev/foo", From 0c5231239749b833b3a23ea7d443f268a51d6f44 Mon Sep 17 00:00:00 2001 From: tamarzanzouri Date: Mon, 11 Mar 2024 21:47:03 +0200 Subject: [PATCH 09/31] fixed failing test module control --- api/src/opentrons/drivers/rpi_drivers/usb.py | 2 +- .../hardware_control/test_module_control.py | 23 +++++++++++++------ 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/api/src/opentrons/drivers/rpi_drivers/usb.py b/api/src/opentrons/drivers/rpi_drivers/usb.py index 16786cd68a6..04ee5496c4a 100644 --- a/api/src/opentrons/drivers/rpi_drivers/usb.py +++ b/api/src/opentrons/drivers/rpi_drivers/usb.py @@ -92,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/tests/opentrons/hardware_control/test_module_control.py b/api/tests/opentrons/hardware_control/test_module_control.py index 752344954e6..57dcc393563 100644 --- a/api/tests/opentrons/hardware_control/test_module_control.py +++ b/api/tests/opentrons/hardware_control/test_module_control.py @@ -1,7 +1,7 @@ """Tests for opentrons.hardware_control.module_control.""" import pytest from decoy import Decoy, matchers -from typing import Awaitable, Callable, cast, Union +from typing import Awaitable, Callable, cast, Union, List from opentrons.drivers.rpi_drivers.types import USBPort from opentrons.drivers.rpi_drivers.interfaces import USBDriverInterface @@ -62,8 +62,14 @@ def subject( @pytest.mark.parametrize( "module_at_port_input", [ - (ModuleAtPort(port="/dev/foo", name="bar")), - SimulatingModuleAtPort(port="/dev/foo", name="bar", serial_number="test-123"), + ([ModuleAtPort(port="/dev/foo", name="bar")]), + ( + [ + SimulatingModuleAtPort( + port="/dev/foo", name="bar", serial_number="test-123" + ) + ] + ), ], ) async def test_register_modules( @@ -72,10 +78,9 @@ async def test_register_modules( build_module: Callable[..., Awaitable[AbstractModule]], hardware_api: HardwareAPI, subject: AttachedModulesControl, - module_at_port_input: Union[ModuleAtPort, SimulatingModuleAtPort], + module_at_port_input: Union[List[ModuleAtPort], List[SimulatingModuleAtPort]], ) -> None: """It should register attached modules.""" - new_mods_at_ports = [module_at_port_input] actual_ports = [ ModuleAtPort( port="/dev/foo", @@ -87,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, + 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] @@ -142,6 +150,7 @@ async def test_register_modules_sort( usb_port=mod.usb_port, port=matchers.Anything(), type=matchers.Anything(), + serial_number=None, ) ).then_return(mod) From 58e3b0b38557d7c19d0952197a969710d4a74f42 Mon Sep 17 00:00:00 2001 From: tamarzanzouri Date: Mon, 11 Mar 2024 22:28:30 +0200 Subject: [PATCH 10/31] fix up structure - WIP --- .../hardware_control/simulator_setup.py | 14 +++++--- .../hardware_control/test_simulator_setup.py | 32 +++++++++---------- 2 files changed, 25 insertions(+), 21 deletions(-) diff --git a/api/src/opentrons/hardware_control/simulator_setup.py b/api/src/opentrons/hardware_control/simulator_setup.py index 9cea61839d8..f9cedf928f3 100644 --- a/api/src/opentrons/hardware_control/simulator_setup.py +++ b/api/src/opentrons/hardware_control/simulator_setup.py @@ -17,6 +17,7 @@ @dataclass(frozen=True) class ModuleCall: function_name: str + serial_number: str args: List[Any] = field(default_factory=list) kwargs: Dict[str, Any] = field(default_factory=dict) @@ -24,7 +25,6 @@ class ModuleCall: # Name and kwargs for a module function @dataclass(frozen=True) class ModuleItem: - serial_number: str items: List[ModuleCall] = field(default_factory=list) @@ -205,14 +205,18 @@ 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: + attached_modules: Dict[str, ModuleItem] = {} # list of items with id, item - loop and set to ModuleCall for key, item in value.items(): - # print(list(item['items'])) for obj in item: for data in obj["items"]: - print(ModuleCall(**data)) + attached_modules.setdefault(key, ModuleItem().items).append( + ModuleCall(**data) + ) + + print(attached_modules) + print("----") # items = {k: [ModuleCall(**data) for data in v] for (k, v) in value.items()} - # print(items) - return {k: [ModuleCall(**data) for data in v] for (k, v) in value.items()} + return attached_modules return value diff --git a/api/tests/opentrons/hardware_control/test_simulator_setup.py b/api/tests/opentrons/hardware_control/test_simulator_setup.py index 44bda189d81..34903f0201d 100644 --- a/api/tests/opentrons/hardware_control/test_simulator_setup.py +++ b/api/tests/opentrons/hardware_control/test_simulator_setup.py @@ -164,30 +164,27 @@ def test_persistence_ot3(tmpdir: str) -> None: attached_modules={ "magdeck": [ simulator_setup.ModuleItem( - id="mag-123", - items=[simulator_setup.ModuleCall("engage", kwargs={"height": 3})], - ) - ], - "tempdeck": [ - simulator_setup.ModuleItem( - id="temp-123", items=[ simulator_setup.ModuleCall( - "set_temperature", kwargs={"celsius": 23} - ), - simulator_setup.ModuleCall( - "set_temperature", kwargs={"celsius": 24} - ), + function_name="engage", + serial_number="mag-1", + kwargs={"height": 3}, + ) ], - ), + ) + ], + "tempdeck": [ simulator_setup.ModuleItem( - id="temp-1234", items=[ simulator_setup.ModuleCall( - "set_temperature", kwargs={"celsius": 23} + function_name="set_temperature", + serial_number="temp-1", + kwargs={"celsius": 23}, ), simulator_setup.ModuleCall( - "set_temperature", kwargs={"celsius": 24} + function_name="set_temperature", + serial_number="temp-2", + kwargs={"celsius": 24}, ), ], ), @@ -199,4 +196,7 @@ def test_persistence_ot3(tmpdir: str) -> None: simulator_setup.save_simulator_setup(sim, file) test_sim = simulator_setup.load_simulator_setup(file) + print(sim.attached_modules) + print("====") + print(test_sim.attached_modules) assert test_sim == sim From 3be706b2075f6ad6cd2a61629d94cc30a2e30380 Mon Sep 17 00:00:00 2001 From: tamarzanzouri Date: Mon, 11 Mar 2024 23:33:51 +0200 Subject: [PATCH 11/31] fixed simulator_setup.py structure and test --- .../hardware_control/simulator_setup.py | 17 +++++++-------- .../hardware_control/test_simulator_setup.py | 21 +++++++++++++------ 2 files changed, 22 insertions(+), 16 deletions(-) diff --git a/api/src/opentrons/hardware_control/simulator_setup.py b/api/src/opentrons/hardware_control/simulator_setup.py index f9cedf928f3..ef9255f4e13 100644 --- a/api/src/opentrons/hardware_control/simulator_setup.py +++ b/api/src/opentrons/hardware_control/simulator_setup.py @@ -17,7 +17,6 @@ @dataclass(frozen=True) class ModuleCall: function_name: str - serial_number: str args: List[Any] = field(default_factory=list) kwargs: Dict[str, Any] = field(default_factory=dict) @@ -25,6 +24,7 @@ class ModuleCall: # Name and kwargs for a module function @dataclass(frozen=True) class ModuleItem: + serial_number: str items: List[ModuleCall] = field(default_factory=list) @@ -205,18 +205,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: - attached_modules: Dict[str, ModuleItem] = {} - # list of items with id, item - loop and set to ModuleCall + attached_modules: Dict[str, List[ModuleItem]] = {} for key, item in value.items(): for obj in item: - for data in obj["items"]: - attached_modules.setdefault(key, ModuleItem().items).append( - ModuleCall(**data) + attached_modules.setdefault(key, []).append( + ModuleItem( + serial_number=obj["serial_number"], + items=[ModuleCall(**data) for data in obj["items"]], ) + ) - print(attached_modules) - print("----") - - # items = {k: [ModuleCall(**data) for data in v] for (k, v) in value.items()} return attached_modules return value diff --git a/api/tests/opentrons/hardware_control/test_simulator_setup.py b/api/tests/opentrons/hardware_control/test_simulator_setup.py index 34903f0201d..75f0187655e 100644 --- a/api/tests/opentrons/hardware_control/test_simulator_setup.py +++ b/api/tests/opentrons/hardware_control/test_simulator_setup.py @@ -164,10 +164,10 @@ def test_persistence_ot3(tmpdir: str) -> None: attached_modules={ "magdeck": [ simulator_setup.ModuleItem( + serial_number="mag-1", items=[ simulator_setup.ModuleCall( function_name="engage", - serial_number="mag-1", kwargs={"height": 3}, ) ], @@ -175,15 +175,27 @@ def test_persistence_ot3(tmpdir: str) -> None: ], "tempdeck": [ simulator_setup.ModuleItem( + serial_number="temp-1", + items=[ + 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", items=[ simulator_setup.ModuleCall( function_name="set_temperature", - serial_number="temp-1", kwargs={"celsius": 23}, ), simulator_setup.ModuleCall( function_name="set_temperature", - serial_number="temp-2", kwargs={"celsius": 24}, ), ], @@ -196,7 +208,4 @@ def test_persistence_ot3(tmpdir: str) -> None: simulator_setup.save_simulator_setup(sim, file) test_sim = simulator_setup.load_simulator_setup(file) - print(sim.attached_modules) - print("====") - print(test_sim.attached_modules) assert test_sim == sim From 01aee674599874e9a1def9b4ea760edde9d669df Mon Sep 17 00:00:00 2001 From: TamarZanzouri Date: Wed, 13 Mar 2024 08:24:03 -0400 Subject: [PATCH 12/31] Update api/src/opentrons/hardware_control/module_control.py --- api/src/opentrons/hardware_control/module_control.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/src/opentrons/hardware_control/module_control.py b/api/src/opentrons/hardware_control/module_control.py index 254885000ee..fde196ea760 100644 --- a/api/src/opentrons/hardware_control/module_control.py +++ b/api/src/opentrons/hardware_control/module_control.py @@ -86,7 +86,7 @@ async def build_module( usb_port: types.USBPort, type: modules.ModuleType, sim_model: Optional[str] = None, - serial_number: Optional[str] = None, + sim_serial_number: Optional[str] = None, ) -> modules.AbstractModule: return await modules.build( port=port, From 49aaff7dff71d969fbc7396b4b42f9a03f625c1b Mon Sep 17 00:00:00 2001 From: TamarZanzouri Date: Wed, 13 Mar 2024 08:24:26 -0400 Subject: [PATCH 13/31] Update api/src/opentrons/hardware_control/modules/heater_shaker.py --- api/src/opentrons/hardware_control/modules/heater_shaker.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/src/opentrons/hardware_control/modules/heater_shaker.py b/api/src/opentrons/hardware_control/modules/heater_shaker.py index df23c2e8ef5..cd7e2461b35 100644 --- a/api/src/opentrons/hardware_control/modules/heater_shaker.py +++ b/api/src/opentrons/hardware_control/modules/heater_shaker.py @@ -49,7 +49,7 @@ async def build( poll_interval_seconds: Optional[float] = None, simulating: bool = False, sim_model: Optional[str] = None, - serial_number: Optional[str] = None, + sim_serial_number: Optional[str] = None, ) -> "HeaterShaker": """ Build a HeaterShaker From a5e359ac4cf613c90906226165b69d9185023fc2 Mon Sep 17 00:00:00 2001 From: TamarZanzouri Date: Wed, 13 Mar 2024 08:25:02 -0400 Subject: [PATCH 14/31] Update api/src/opentrons/hardware_control/modules/utils.py --- api/src/opentrons/hardware_control/modules/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/src/opentrons/hardware_control/modules/utils.py b/api/src/opentrons/hardware_control/modules/utils.py index 557c2c4bbf8..c8a696aa790 100644 --- a/api/src/opentrons/hardware_control/modules/utils.py +++ b/api/src/opentrons/hardware_control/modules/utils.py @@ -42,7 +42,7 @@ async def build( hw_control_loop: asyncio.AbstractEventLoop, execution_manager: ExecutionManager, sim_model: Optional[str] = None, - serial_number: Optional[str] = None, + sim_serial_number: Optional[str] = None, ) -> AbstractModule: return await _MODULE_CLS_BY_TYPE[type].build( port=port, From c5220d48335e59f997344915d0122cd593c316e1 Mon Sep 17 00:00:00 2001 From: tamarzanzouri Date: Wed, 13 Mar 2024 14:42:27 +0200 Subject: [PATCH 15/31] fixed typo, sim_serial_number, and looping through calls --- api/src/opentrons/hardware_control/module_control.py | 4 ++-- .../hardware_control/modules/heater_shaker.py | 2 +- api/src/opentrons/hardware_control/modules/utils.py | 2 +- api/src/opentrons/hardware_control/simulator_setup.py | 11 +++++++---- 4 files changed, 11 insertions(+), 8 deletions(-) diff --git a/api/src/opentrons/hardware_control/module_control.py b/api/src/opentrons/hardware_control/module_control.py index fde196ea760..d2046c29eca 100644 --- a/api/src/opentrons/hardware_control/module_control.py +++ b/api/src/opentrons/hardware_control/module_control.py @@ -96,7 +96,7 @@ async def build_module( hw_control_loop=self._api.loop, execution_manager=self._api._execution_manager, sim_model=sim_model, - serial_number=serial_number, + sim_serial_number=sim_serial_number, ) async def unregister_modules( @@ -158,7 +158,7 @@ async def register_modules( port=mod.port, usb_port=mod.usb_port, type=modules.MODULE_TYPE_BY_NAME[mod.name], - serial_number=mod.serial_number + sim_serial_number=mod.serial_number if isinstance(mod, SimulatingModuleAtPort) else None, ) diff --git a/api/src/opentrons/hardware_control/modules/heater_shaker.py b/api/src/opentrons/hardware_control/modules/heater_shaker.py index cd7e2461b35..09ac06ea5f2 100644 --- a/api/src/opentrons/hardware_control/modules/heater_shaker.py +++ b/api/src/opentrons/hardware_control/modules/heater_shaker.py @@ -72,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(serial_number=serial_number) + 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/utils.py b/api/src/opentrons/hardware_control/modules/utils.py index c8a696aa790..152f4183a61 100644 --- a/api/src/opentrons/hardware_control/modules/utils.py +++ b/api/src/opentrons/hardware_control/modules/utils.py @@ -51,7 +51,7 @@ async def build( hw_control_loop=hw_control_loop, execution_manager=execution_manager, sim_model=sim_model, - serial_number=serial_number, + serial_number=sim_serial_number, ) diff --git a/api/src/opentrons/hardware_control/simulator_setup.py b/api/src/opentrons/hardware_control/simulator_setup.py index ef9255f4e13..405296d08c3 100644 --- a/api/src/opentrons/hardware_control/simulator_setup.py +++ b/api/src/opentrons/hardware_control/simulator_setup.py @@ -132,10 +132,13 @@ 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()] + print(modules) + for module in modules: + print(module) + for call in module.items: + f = getattr(attached_module, call.function_name) + await f(*call.args, **call.kwargs) return thread_manager From b106f5f0181c6e3be8a414d201f55cc9dd2603b2 Mon Sep 17 00:00:00 2001 From: tamarzanzouri Date: Wed, 13 Mar 2024 23:06:49 +0200 Subject: [PATCH 16/31] changed attached_modules to accepts sn --- .../hardware_control/backends/ot3simulator.py | 4 ++-- api/src/opentrons/hardware_control/ot3api.py | 4 ++-- .../hardware_control/simulator_setup.py | 16 ++++++++++------ 3 files changed, 14 insertions(+), 10 deletions(-) diff --git a/api/src/opentrons/hardware_control/backends/ot3simulator.py b/api/src/opentrons/hardware_control/backends/ot3simulator.py index 07eaaadda00..09afd7f00ea 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, 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 405296d08c3..2b0f4cc3982 100644 --- a/api/src/opentrons/hardware_control/simulator_setup.py +++ b/api/src/opentrons/hardware_control/simulator_setup.py @@ -34,7 +34,7 @@ class OT2SimulatorSetup: 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 @@ -45,7 +45,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 @@ -70,7 +70,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, @@ -117,7 +120,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(), @@ -133,9 +139,7 @@ async def create_simulator_thread_manager( for attached_module in thread_manager.wrapped().attached_modules: modules = setup.attached_modules[attached_module.name()] - print(modules) for module in modules: - print(module) for call in module.items: f = getattr(attached_module, call.function_name) await f(*call.args, **call.kwargs) From f882ae728d28077851f43def3ba4511a59136827 Mon Sep 17 00:00:00 2001 From: tamarzanzouri Date: Tue, 19 Mar 2024 12:48:30 -0400 Subject: [PATCH 17/31] fixed failing tests --- .../hardware_control/simulator_setup.py | 14 +++--- .../hardware_control/test_simulator_setup.py | 45 ++++++++++++------- 2 files changed, 39 insertions(+), 20 deletions(-) diff --git a/api/src/opentrons/hardware_control/simulator_setup.py b/api/src/opentrons/hardware_control/simulator_setup.py index 2b0f4cc3982..82ad126e335 100644 --- a/api/src/opentrons/hardware_control/simulator_setup.py +++ b/api/src/opentrons/hardware_control/simulator_setup.py @@ -87,10 +87,11 @@ 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: + for call in module.items: + f = getattr(attached_module, call.function_name) + await f(*call.args, **call.kwargs) return simulator @@ -109,7 +110,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(), diff --git a/api/tests/opentrons/hardware_control/test_simulator_setup.py b/api/tests/opentrons/hardware_control/test_simulator_setup.py index 75f0187655e..4dd3e63f84f 100644 --- a/api/tests/opentrons/hardware_control/test_simulator_setup.py +++ b/api/tests/opentrons/hardware_control/test_simulator_setup.py @@ -56,7 +56,12 @@ 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", + items=[simulator_setup.ModuleCall("engage", kwargs={"height": 3})], + ) + ] } ) simulator = await simulator_setup.create_simulator(setup) @@ -75,14 +80,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", + items=[ + simulator_setup.ModuleCall( + "set_temperature", + kwargs={ + "temperature": 3, + "hold_time_seconds": 1, + "hold_time_minutes": 2, + "volume": 5, + }, + ) + ], ) ] } @@ -114,12 +124,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", + items=[ + simulator_setup.ModuleCall( + "start_set_temperature", kwargs={"celsius": 23} + ), + simulator_setup.ModuleCall( + "await_temperature", kwargs={"awaiting_temperature": None} + ), + ], + ) ] } ) From 63de4f25b4b3d61ba2a569329d484b4c41569709 Mon Sep 17 00:00:00 2001 From: tamarzanzouri Date: Tue, 19 Mar 2024 14:18:25 -0400 Subject: [PATCH 18/31] fixed ot3 full path to set sn --- .../hardware_control/backends/ot3simulator.py | 14 ++++++++++---- .../hardware_control/test_simulator_setup.py | 3 +++ 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/api/src/opentrons/hardware_control/backends/ot3simulator.py b/api/src/opentrons/hardware_control/backends/ot3simulator.py index 09afd7f00ea..f57f02e8c51 100644 --- a/api/src/opentrons/hardware_control/backends/ot3simulator.py +++ b/api/src/opentrons/hardware_control/backends/ot3simulator.py @@ -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/tests/opentrons/hardware_control/test_simulator_setup.py b/api/tests/opentrons/hardware_control/test_simulator_setup.py index 4dd3e63f84f..ce95b231a25 100644 --- a/api/tests/opentrons/hardware_control/test_simulator_setup.py +++ b/api/tests/opentrons/hardware_control/test_simulator_setup.py @@ -71,6 +71,7 @@ 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" async def test_with_thermocycler( @@ -117,6 +118,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: @@ -145,6 +147,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: From 0b95d7cf68d33db1961fca6bd4075c27d06307bd Mon Sep 17 00:00:00 2001 From: tamarzanzouri Date: Tue, 19 Mar 2024 14:26:44 -0400 Subject: [PATCH 19/31] fixed ot2 full sn assigments --- api/src/opentrons/hardware_control/api.py | 4 ++-- .../hardware_control/backends/simulator.py | 18 ++++++++++++------ .../hardware_control/simulator_setup.py | 5 ++++- 3 files changed, 18 insertions(+), 9 deletions(-) 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/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/simulator_setup.py b/api/src/opentrons/hardware_control/simulator_setup.py index 82ad126e335..3740730d6a6 100644 --- a/api/src/opentrons/hardware_control/simulator_setup.py +++ b/api/src/opentrons/hardware_control/simulator_setup.py @@ -59,7 +59,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, From 56f7f82685c437636c67eca00e6eebcc2b73c306 Mon Sep 17 00:00:00 2001 From: TamarZanzouri Date: Tue, 19 Mar 2024 15:35:55 -0400 Subject: [PATCH 20/31] Update api/src/opentrons/hardware_control/simulator_setup.py --- api/src/opentrons/hardware_control/simulator_setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/src/opentrons/hardware_control/simulator_setup.py b/api/src/opentrons/hardware_control/simulator_setup.py index 3740730d6a6..12101b59669 100644 --- a/api/src/opentrons/hardware_control/simulator_setup.py +++ b/api/src/opentrons/hardware_control/simulator_setup.py @@ -25,7 +25,7 @@ class ModuleCall: @dataclass(frozen=True) class ModuleItem: serial_number: str - items: List[ModuleCall] = field(default_factory=list) + calls: List[ModuleCall] = field(default_factory=list) @dataclass(frozen=True) From e7f468f1c98440bdcf5969a5effabee45547a8d0 Mon Sep 17 00:00:00 2001 From: TamarZanzouri Date: Tue, 19 Mar 2024 15:36:10 -0400 Subject: [PATCH 21/31] Update robot-server/simulators/test-flex.json --- robot-server/simulators/test-flex.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/robot-server/simulators/test-flex.json b/robot-server/simulators/test-flex.json index 7859e239e6c..e8ed77e93d2 100644 --- a/robot-server/simulators/test-flex.json +++ b/robot-server/simulators/test-flex.json @@ -37,7 +37,7 @@ "tempdeck": [ { "serial_number": "temp-123", - "items": [ + "calls": [ { "function_name": "start_set_temperature", "kwargs": { From 901d16220e2aa7cca54b9c080952eabf38ee27fb Mon Sep 17 00:00:00 2001 From: tamarzanzouri Date: Tue, 19 Mar 2024 16:51:01 -0400 Subject: [PATCH 22/31] wip pr feedback --- api/src/opentrons/hardware_control/modules/mod_abc.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/src/opentrons/hardware_control/modules/mod_abc.py b/api/src/opentrons/hardware_control/modules/mod_abc.py index 72b6b47969e..c6ea41437eb 100644 --- a/api/src/opentrons/hardware_control/modules/mod_abc.py +++ b/api/src/opentrons/hardware_control/modules/mod_abc.py @@ -32,7 +32,7 @@ async def build( poll_interval_seconds: Optional[float] = None, simulating: bool = False, sim_model: Optional[str] = None, - serial_number: Optional[str] = None, + sim_serial_number: Optional[str] = None, ) -> "AbstractModule": """Modules should always be created using this factory. From a339f6ce5c0fa4ac296e18208f91e58996498b74 Mon Sep 17 00:00:00 2001 From: tamarzanzouri Date: Tue, 19 Mar 2024 16:51:18 -0400 Subject: [PATCH 23/31] wip pr feedback --- api/src/opentrons/hardware_control/modules/utils.py | 2 +- api/src/opentrons/hardware_control/simulator_setup.py | 6 +++--- .../opentrons/hardware_control/test_module_control.py | 4 ++-- api/tests/opentrons/hardware_control/test_modules.py | 9 +++++---- 4 files changed, 11 insertions(+), 10 deletions(-) diff --git a/api/src/opentrons/hardware_control/modules/utils.py b/api/src/opentrons/hardware_control/modules/utils.py index 152f4183a61..0c213ead6a1 100644 --- a/api/src/opentrons/hardware_control/modules/utils.py +++ b/api/src/opentrons/hardware_control/modules/utils.py @@ -51,7 +51,7 @@ async def build( hw_control_loop=hw_control_loop, execution_manager=execution_manager, sim_model=sim_model, - serial_number=sim_serial_number, + sim_serial_number=sim_serial_number, ) diff --git a/api/src/opentrons/hardware_control/simulator_setup.py b/api/src/opentrons/hardware_control/simulator_setup.py index 12101b59669..7bd39d58e78 100644 --- a/api/src/opentrons/hardware_control/simulator_setup.py +++ b/api/src/opentrons/hardware_control/simulator_setup.py @@ -92,7 +92,7 @@ async def create_simulator( for attached_module in simulator.attached_modules: modules = setup.attached_modules[attached_module.name()] for module in modules: - for call in module.items: + for call in module.calls: f = getattr(attached_module, call.function_name) await f(*call.args, **call.kwargs) @@ -147,7 +147,7 @@ async def create_simulator_thread_manager( for attached_module in thread_manager.wrapped().attached_modules: modules = setup.attached_modules[attached_module.name()] for module in modules: - for call in module.items: + for call in module.calls: f = getattr(attached_module, call.function_name) await f(*call.args, **call.kwargs) @@ -225,7 +225,7 @@ def _prepare_for_ot3_simulator_setup(key: str, value: Dict[str, Any]) -> Any: attached_modules.setdefault(key, []).append( ModuleItem( serial_number=obj["serial_number"], - items=[ModuleCall(**data) for data in obj["items"]], + calls=[ModuleCall(**data) for data in obj["calls"]], ) ) diff --git a/api/tests/opentrons/hardware_control/test_module_control.py b/api/tests/opentrons/hardware_control/test_module_control.py index 57dcc393563..36fd6cb1793 100644 --- a/api/tests/opentrons/hardware_control/test_module_control.py +++ b/api/tests/opentrons/hardware_control/test_module_control.py @@ -100,7 +100,7 @@ async def test_register_modules( port="/dev/foo", usb_port=USBPort(name="baz", port_number=0), type=ModuleType.TEMPERATURE, - serial_number=None, + sim_serial_number=None, ) ).then_return(module) @@ -150,7 +150,7 @@ async def test_register_modules_sort( usb_port=mod.usb_port, port=matchers.Anything(), type=matchers.Anything(), - serial_number=None, + 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..ea9a477cfc7 100644 --- a/api/tests/opentrons/hardware_control/test_modules.py +++ b/api/tests/opentrons/hardware_control/test_modules.py @@ -28,7 +28,7 @@ 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 +40,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 ) @@ -94,7 +94,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 +343,8 @@ 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) From e5d313ee58d003949a04289db9ce3f450c5871a4 Mon Sep 17 00:00:00 2001 From: tamarzanzouri Date: Wed, 20 Mar 2024 15:34:54 -0400 Subject: [PATCH 24/31] fixed failing test with port name change and sim_serial_number in modules --- .../hardware_control/module_control.py | 6 +++++- .../hardware_control/modules/magdeck.py | 6 ++++-- .../hardware_control/modules/tempdeck.py | 6 ++++-- .../hardware_control/modules/thermocycler.py | 4 ++-- .../opentrons/hardware_control/test_modules.py | 17 ++++++++++++++--- 5 files changed, 29 insertions(+), 10 deletions(-) diff --git a/api/src/opentrons/hardware_control/module_control.py b/api/src/opentrons/hardware_control/module_control.py index d2046c29eca..24f960ee1e7 100644 --- a/api/src/opentrons/hardware_control/module_control.py +++ b/api/src/opentrons/hardware_control/module_control.py @@ -100,7 +100,10 @@ async def build_module( ) 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. @@ -108,6 +111,7 @@ async def unregister_modules( Remove any modules that are no longer found by aionotify. """ removed_modules = [] + for mod in mods_at_ports: for attached_mod in self.available_modules: if attached_mod.port == mod.port: diff --git a/api/src/opentrons/hardware_control/modules/magdeck.py b/api/src/opentrons/hardware_control/modules/magdeck.py index 7f0293485f3..07c0f2ffb5c 100644 --- a/api/src/opentrons/hardware_control/modules/magdeck.py +++ b/api/src/opentrons/hardware_control/modules/magdeck.py @@ -53,14 +53,16 @@ async def build( poll_interval_seconds: Optional[float] = None, simulating: bool = False, sim_model: Optional[str] = None, - serial_number: 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, serial_number=serial_number) + driver = SimulatingDriver( + sim_model=sim_model, serial_number=sim_serial_number + ) mod = cls( port=port, diff --git a/api/src/opentrons/hardware_control/modules/tempdeck.py b/api/src/opentrons/hardware_control/modules/tempdeck.py index 1fb4ad3a64e..afcc4d64636 100644 --- a/api/src/opentrons/hardware_control/modules/tempdeck.py +++ b/api/src/opentrons/hardware_control/modules/tempdeck.py @@ -39,7 +39,7 @@ async def build( poll_interval_seconds: Optional[float] = None, simulating: bool = False, sim_model: Optional[str] = None, - serial_number: Optional[str] = None, + sim_serial_number: Optional[str] = None, ) -> "TempDeck": """ Build a TempDeck @@ -61,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, serial_number=serial_number) + 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 04242b8555d..f93cd61ded9 100644 --- a/api/src/opentrons/hardware_control/modules/thermocycler.py +++ b/api/src/opentrons/hardware_control/modules/thermocycler.py @@ -63,7 +63,7 @@ async def build( poll_interval_seconds: Optional[float] = None, simulating: bool = False, sim_model: Optional[str] = None, - serial_number: Optional[str] = None, + sim_serial_number: Optional[str] = None, ) -> "Thermocycler": """ Build and connect to a Thermocycler @@ -88,7 +88,7 @@ async def build( ) poll_interval_seconds = poll_interval_seconds or POLLING_FREQUENCY_SEC else: - driver = SimulatingDriver(model=sim_model, serial_number=serial_number) + 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/tests/opentrons/hardware_control/test_modules.py b/api/tests/opentrons/hardware_control/test_modules.py index ea9a477cfc7..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": ["111"], "magdeck": ["222"], "thermocycler": ["333"], "heatershaker": ["444"]} + 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 @@ -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 @@ -343,7 +349,12 @@ async def test_get_bundled_fw(monkeypatch, tmpdir): from opentrons.hardware_control import API - mods = {"tempdeck": ["111"], "magdeck": ["222"], "thermocycler": ["333"], "heatershaker": ["444"]} + mods = { + "tempdeck": ["111"], + "magdeck": ["222"], + "thermocycler": ["333"], + "heatershaker": ["444"], + } api = await API.build_hardware_simulator(attached_modules=mods) await asyncio.sleep(0.05) From ffc4ca1a750d135bb50a69988d16179bbf4228c9 Mon Sep 17 00:00:00 2001 From: tamarzanzouri Date: Wed, 20 Mar 2024 17:02:39 -0400 Subject: [PATCH 25/31] fixed all failing tests --- .../hardware_control/test_simulator_setup.py | 12 ++++++------ .../hardware_control/test_thread_manager.py | 6 +++--- api/tests/opentrons/protocol_api_old/test_context.py | 4 ++-- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/api/tests/opentrons/hardware_control/test_simulator_setup.py b/api/tests/opentrons/hardware_control/test_simulator_setup.py index ce95b231a25..5873c9d0279 100644 --- a/api/tests/opentrons/hardware_control/test_simulator_setup.py +++ b/api/tests/opentrons/hardware_control/test_simulator_setup.py @@ -59,7 +59,7 @@ async def test_with_magdeck(setup_klass: Type[simulator_setup.SimulatorSetup]) - "magdeck": [ simulator_setup.ModuleItem( serial_number="123", - items=[simulator_setup.ModuleCall("engage", kwargs={"height": 3})], + calls=[simulator_setup.ModuleCall("engage", kwargs={"height": 3})], ) ] } @@ -83,7 +83,7 @@ async def test_with_thermocycler( "thermocycler": [ simulator_setup.ModuleItem( serial_number="123", - items=[ + calls=[ simulator_setup.ModuleCall( "set_temperature", kwargs={ @@ -128,7 +128,7 @@ async def test_with_tempdeck(setup_klass: Type[simulator_setup.SimulatorSetup]) "tempdeck": [ simulator_setup.ModuleItem( serial_number="123", - items=[ + calls=[ simulator_setup.ModuleCall( "start_set_temperature", kwargs={"celsius": 23} ), @@ -183,7 +183,7 @@ def test_persistence_ot3(tmpdir: str) -> None: "magdeck": [ simulator_setup.ModuleItem( serial_number="mag-1", - items=[ + calls=[ simulator_setup.ModuleCall( function_name="engage", kwargs={"height": 3}, @@ -194,7 +194,7 @@ def test_persistence_ot3(tmpdir: str) -> None: "tempdeck": [ simulator_setup.ModuleItem( serial_number="temp-1", - items=[ + calls=[ simulator_setup.ModuleCall( function_name="set_temperature", kwargs={"celsius": 23}, @@ -207,7 +207,7 @@ def test_persistence_ot3(tmpdir: str) -> None: ), simulator_setup.ModuleItem( serial_number="temp-2", - items=[ + calls=[ simulator_setup.ModuleCall( function_name="set_temperature", kwargs={"celsius": 23}, 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), From ec71a422c74059c7d106a4c77250cd0ef32de397 Mon Sep 17 00:00:00 2001 From: tamarzanzouri Date: Wed, 20 Mar 2024 21:06:35 -0400 Subject: [PATCH 26/31] check sn when making calls on module --- api/src/opentrons/hardware_control/simulator_setup.py | 7 ++++--- .../hardware_control/test_simulator_setup.py | 11 ++++++++++- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/api/src/opentrons/hardware_control/simulator_setup.py b/api/src/opentrons/hardware_control/simulator_setup.py index 7bd39d58e78..50e9d1fee1a 100644 --- a/api/src/opentrons/hardware_control/simulator_setup.py +++ b/api/src/opentrons/hardware_control/simulator_setup.py @@ -92,9 +92,10 @@ async def create_simulator( for attached_module in simulator.attached_modules: 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) + 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 diff --git a/api/tests/opentrons/hardware_control/test_simulator_setup.py b/api/tests/opentrons/hardware_control/test_simulator_setup.py index 5873c9d0279..7e5794b2056 100644 --- a/api/tests/opentrons/hardware_control/test_simulator_setup.py +++ b/api/tests/opentrons/hardware_control/test_simulator_setup.py @@ -60,7 +60,11 @@ async def test_with_magdeck(setup_klass: Type[simulator_setup.SimulatorSetup]) - 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})], + ), ] } ) @@ -72,6 +76,11 @@ async def test_with_magdeck(setup_klass: Type[simulator_setup.SimulatorSetup]) - "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( From fdcb2ad5d4044861008c0ba987d67d38ef8d6852 Mon Sep 17 00:00:00 2001 From: tamarzanzouri Date: Wed, 20 Mar 2024 21:58:19 -0400 Subject: [PATCH 27/31] formatting json files, ot-2 config and simulator for ot2 --- .../hardware_control/simulator_setup.py | 13 ++- .../hardware_control/test_simulator_setup.py | 20 +++- robot-server/simulators/test-flex.json | 103 +++++++++--------- robot-server/simulators/test.json | 84 +++++++++----- 4 files changed, 138 insertions(+), 82 deletions(-) diff --git a/api/src/opentrons/hardware_control/simulator_setup.py b/api/src/opentrons/hardware_control/simulator_setup.py index 50e9d1fee1a..25063934b2b 100644 --- a/api/src/opentrons/hardware_control/simulator_setup.py +++ b/api/src/opentrons/hardware_control/simulator_setup.py @@ -210,7 +210,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 diff --git a/api/tests/opentrons/hardware_control/test_simulator_setup.py b/api/tests/opentrons/hardware_control/test_simulator_setup.py index 7e5794b2056..2507a9969b3 100644 --- a/api/tests/opentrons/hardware_control/test_simulator_setup.py +++ b/api/tests/opentrons/hardware_control/test_simulator_setup.py @@ -166,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({}), diff --git a/robot-server/simulators/test-flex.json b/robot-server/simulators/test-flex.json index e8ed77e93d2..9b5801226d5 100644 --- a/robot-server/simulators/test-flex.json +++ b/robot-server/simulators/test-flex.json @@ -14,23 +14,25 @@ "attached_modules": { "thermocycler": [ { - "serial_number": "123", - "items": [{ - "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 - } - }] + "serial_number": "therm-123", + "items": [ + { + "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": [], @@ -38,47 +40,50 @@ { "serial_number": "temp-123", "calls": [ - { - "function_name": "start_set_temperature", - "kwargs": { - "celsius": 3 - } + { + "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", "items": [ - { - "function_name": "start_set_temperature", - "kwargs": { - "celsius": 3 - } - }, - { - "function_name": "await_temperature", - "kwargs": { - "awaiting_temperature": null - } + { + "function_name": "start_set_temperature", + "kwargs": { + "celsius": 3 + } + }, + { + "function_name": "await_temperature", + "kwargs": { + "awaiting_temperature": null + } + } + ] } - ]} ], "magdeck": [ { - "serial_number": "temp-123", + "serial_number": "mag-123", "items": [ - { - "function_name": "engage", - "kwargs": { - "height": 4 - } + { + "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..59e4ab34ac3 100644 --- a/robot-server/simulators/test.json +++ b/robot-server/simulators/test.json @@ -12,44 +12,70 @@ "attached_modules": { "thermocycler": [ { - "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 - } + "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": [], "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 From 358f5c4092dfd4a609ba0c24a0514094d764ce94 Mon Sep 17 00:00:00 2001 From: TamarZanzouri Date: Wed, 20 Mar 2024 22:01:24 -0400 Subject: [PATCH 28/31] Update api/src/opentrons/hardware_control/module_control.py --- api/src/opentrons/hardware_control/module_control.py | 1 - 1 file changed, 1 deletion(-) diff --git a/api/src/opentrons/hardware_control/module_control.py b/api/src/opentrons/hardware_control/module_control.py index 24f960ee1e7..1d32731d026 100644 --- a/api/src/opentrons/hardware_control/module_control.py +++ b/api/src/opentrons/hardware_control/module_control.py @@ -111,7 +111,6 @@ async def unregister_modules( Remove any modules that are no longer found by aionotify. """ removed_modules = [] - for mod in mods_at_ports: for attached_mod in self.available_modules: if attached_mod.port == mod.port: From 5aae58e7cc4b176780e8547e3dc2d2b6563626fd Mon Sep 17 00:00:00 2001 From: TamarZanzouri Date: Wed, 20 Mar 2024 22:02:52 -0400 Subject: [PATCH 29/31] Update api/src/opentrons/hardware_control/simulator_setup.py --- api/src/opentrons/hardware_control/simulator_setup.py | 1 - 1 file changed, 1 deletion(-) diff --git a/api/src/opentrons/hardware_control/simulator_setup.py b/api/src/opentrons/hardware_control/simulator_setup.py index 25063934b2b..25fa17d36a1 100644 --- a/api/src/opentrons/hardware_control/simulator_setup.py +++ b/api/src/opentrons/hardware_control/simulator_setup.py @@ -21,7 +21,6 @@ class ModuleCall: kwargs: Dict[str, Any] = field(default_factory=dict) -# Name and kwargs for a module function @dataclass(frozen=True) class ModuleItem: serial_number: str From bbbf1748baab4619eca12c829b14175242e924ab Mon Sep 17 00:00:00 2001 From: tamarzanzouri Date: Thu, 21 Mar 2024 10:55:11 -0400 Subject: [PATCH 30/31] fixed missing hs in tests --- robot-server/simulators/test-flex.json | 7 ++++- robot-server/simulators/test.json | 7 ++++- .../integration/test_modules.tavern.yaml | 30 +++++++++++++++++++ 3 files changed, 42 insertions(+), 2 deletions(-) diff --git a/robot-server/simulators/test-flex.json b/robot-server/simulators/test-flex.json index 9b5801226d5..6cff3e19a32 100644 --- a/robot-server/simulators/test-flex.json +++ b/robot-server/simulators/test-flex.json @@ -35,7 +35,12 @@ ] } ], - "heatershaker": [], + "heatershaker": [ + { + "serial_number": "hs-123", + "calls": [] + } + ], "tempdeck": [ { "serial_number": "temp-123", diff --git a/robot-server/simulators/test.json b/robot-server/simulators/test.json index 59e4ab34ac3..c7ca49e9040 100644 --- a/robot-server/simulators/test.json +++ b/robot-server/simulators/test.json @@ -33,7 +33,12 @@ ] } ], - "heatershaker": [], + "heatershaker": [ + { + "serial_number": "hs-123", + "calls": [] + } + ], "tempdeck": [ { "serial_number": "temp-123", 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 From 880c86f6698bfa98357d1380eb42ae72868dc853 Mon Sep 17 00:00:00 2001 From: tamarzanzouri Date: Thu, 21 Mar 2024 12:19:44 -0400 Subject: [PATCH 31/31] changed items to calls in config --- robot-server/simulators/test-flex.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/robot-server/simulators/test-flex.json b/robot-server/simulators/test-flex.json index 6cff3e19a32..d7fc860c662 100644 --- a/robot-server/simulators/test-flex.json +++ b/robot-server/simulators/test-flex.json @@ -15,7 +15,7 @@ "thermocycler": [ { "serial_number": "therm-123", - "items": [ + "calls": [ { "function_name": "set_temperature", "kwargs": { @@ -61,7 +61,7 @@ }, { "serial_number": "temp-1234", - "items": [ + "calls": [ { "function_name": "start_set_temperature", "kwargs": { @@ -80,7 +80,7 @@ "magdeck": [ { "serial_number": "mag-123", - "items": [ + "calls": [ { "function_name": "engage", "kwargs": {