diff --git a/DEV_SETUP.md b/DEV_SETUP.md index cd618d8bdbd..238f2c7fda3 100644 --- a/DEV_SETUP.md +++ b/DEV_SETUP.md @@ -82,7 +82,7 @@ Close and re-open your terminal to confirm `nvs` is installed. nvs --version ``` -Now we can use nvs to install Node.js v18 and switch on `auto` mode, which will make sure Node.js v18 is used any time we're in the `opentrons` project directory. +Now we can use `nvs` to install the currently required Node.js version set in `.nvmrc`. The `auto` command selects the correct version of Node.js any time we're in the `opentrons` project directory. Without `auto`, we would have to manually run `use` or `install` each time we work on the project. ```shell nvs add 18 diff --git a/__mocks__/electron-store.js b/__mocks__/electron-store.js index 51261150343..e4a3ed72bf2 100644 --- a/__mocks__/electron-store.js +++ b/__mocks__/electron-store.js @@ -1,7 +1,6 @@ // mock electron-store 'use strict' import { vi } from 'vitest' -// import { DEFAULTS_V12, migrate } from '../app-shell-odd/src/config/migrate' // will by default mock the config dir. if you need other behaavior you can // override this mock (see app-shell/src/__tests__/discovery.test.ts for an example) diff --git a/api/src/opentrons/protocol_api/core/engine/instrument.py b/api/src/opentrons/protocol_api/core/engine/instrument.py index 1bbe70712ce..6bf569bcd67 100644 --- a/api/src/opentrons/protocol_api/core/engine/instrument.py +++ b/api/src/opentrons/protocol_api/core/engine/instrument.py @@ -33,6 +33,7 @@ from opentrons_shared_data.pipette.dev_types import PipetteNameType from opentrons.protocol_api._nozzle_layout import NozzleLayout from opentrons.hardware_control.nozzle_manager import NozzleConfigurationType +from opentrons.hardware_control.nozzle_manager import NozzleMap from . import deck_conflict from ..instrument import AbstractInstrument @@ -675,6 +676,9 @@ def get_active_channels(self) -> int: self._pipette_id ) + def get_nozzle_map(self) -> NozzleMap: + return self._engine_client.state.tips.get_pipette_nozzle_map(self._pipette_id) + def has_tip(self) -> bool: return ( self._engine_client.state.pipettes.get_attached_tip(self._pipette_id) @@ -709,14 +713,9 @@ def is_tip_tracking_available(self) -> bool: return True else: if self.get_channels() == 96: - # SINGLE configuration with H12 nozzle is technically supported by the - # current tip tracking implementation but we don't do any deck conflict - # checks for it, so we won't provide full support for it yet. - return ( - self.get_nozzle_configuration() == NozzleConfigurationType.COLUMN - and primary_nozzle == "A12" - ) + return True if self.get_channels() == 8: + # TODO: (cb, 03/06/24): Enable automatic tip tracking on the 8 channel pipettes once PAPI support exists return ( self.get_nozzle_configuration() == NozzleConfigurationType.SINGLE and primary_nozzle == "H1" diff --git a/api/src/opentrons/protocol_api/core/engine/labware.py b/api/src/opentrons/protocol_api/core/engine/labware.py index 5190831810c..9b48b309aa2 100644 --- a/api/src/opentrons/protocol_api/core/engine/labware.py +++ b/api/src/opentrons/protocol_api/core/engine/labware.py @@ -11,6 +11,7 @@ from opentrons.protocol_engine.errors import LabwareNotOnDeckError, ModuleNotOnDeckError from opentrons.protocol_engine.clients import SyncClient as ProtocolEngineClient from opentrons.types import DeckSlotName, Point +from opentrons.hardware_control.nozzle_manager import NozzleMap from ..labware import AbstractLabware, LabwareLoadParams from .well import WellCore @@ -122,7 +123,10 @@ def reset_tips(self) -> None: raise TypeError(f"{self.get_display_name()} is not a tip rack.") def get_next_tip( - self, num_tips: int, starting_tip: Optional[WellCore] + self, + num_tips: int, + starting_tip: Optional[WellCore], + nozzle_map: Optional[NozzleMap], ) -> Optional[str]: return self._engine_client.state.tips.get_next_tip( labware_id=self._labware_id, @@ -132,6 +136,7 @@ def get_next_tip( if starting_tip and starting_tip.labware_id == self._labware_id else None ), + nozzle_map=nozzle_map, ) def get_well_columns(self) -> List[List[str]]: diff --git a/api/src/opentrons/protocol_api/core/instrument.py b/api/src/opentrons/protocol_api/core/instrument.py index 1864d308c4f..061e7d13960 100644 --- a/api/src/opentrons/protocol_api/core/instrument.py +++ b/api/src/opentrons/protocol_api/core/instrument.py @@ -9,6 +9,7 @@ from opentrons.hardware_control.dev_types import PipetteDict from opentrons.protocols.api_support.util import FlowRates from opentrons.protocol_api._nozzle_layout import NozzleLayout +from opentrons.hardware_control.nozzle_manager import NozzleMap from ..disposal_locations import TrashBin, WasteChute from .well import WellCoreType @@ -218,6 +219,10 @@ def get_channels(self) -> int: def get_active_channels(self) -> int: ... + @abstractmethod + def get_nozzle_map(self) -> NozzleMap: + ... + @abstractmethod def has_tip(self) -> bool: ... diff --git a/api/src/opentrons/protocol_api/core/labware.py b/api/src/opentrons/protocol_api/core/labware.py index 4411155692f..ada1a7ff0ed 100644 --- a/api/src/opentrons/protocol_api/core/labware.py +++ b/api/src/opentrons/protocol_api/core/labware.py @@ -11,6 +11,7 @@ ) from opentrons.types import DeckSlotName, Point +from opentrons.hardware_control.nozzle_manager import NozzleMap from .well import WellCoreType @@ -110,7 +111,10 @@ def reset_tips(self) -> None: @abstractmethod def get_next_tip( - self, num_tips: int, starting_tip: Optional[WellCoreType] + self, + num_tips: int, + starting_tip: Optional[WellCoreType], + nozzle_map: Optional[NozzleMap], ) -> Optional[str]: """Get the name of the next available tip(s) in the rack, if available.""" diff --git a/api/src/opentrons/protocol_api/core/legacy/legacy_instrument_core.py b/api/src/opentrons/protocol_api/core/legacy/legacy_instrument_core.py index db3ad39e6d9..57f129c32b3 100644 --- a/api/src/opentrons/protocol_api/core/legacy/legacy_instrument_core.py +++ b/api/src/opentrons/protocol_api/core/legacy/legacy_instrument_core.py @@ -18,6 +18,7 @@ ) from opentrons.protocols.geometry import planning from opentrons.protocol_api._nozzle_layout import NozzleLayout +from opentrons.hardware_control.nozzle_manager import NozzleMap from ...disposal_locations import TrashBin, WasteChute from ..instrument import AbstractInstrument @@ -550,6 +551,10 @@ def get_active_channels(self) -> int: """This will never be called because it was added in API 2.16.""" assert False, "get_active_channels only supported in API 2.16 & later" + def get_nozzle_map(self) -> NozzleMap: + """This will never be called because it was added in API 2.18.""" + assert False, "get_nozzle_map only supported in API 2.18 & later" + def is_tip_tracking_available(self) -> bool: # Tip tracking is always available in legacy context return True diff --git a/api/src/opentrons/protocol_api/core/legacy/legacy_labware_core.py b/api/src/opentrons/protocol_api/core/legacy/legacy_labware_core.py index 2749ef8949a..ece9be66f19 100644 --- a/api/src/opentrons/protocol_api/core/legacy/legacy_labware_core.py +++ b/api/src/opentrons/protocol_api/core/legacy/legacy_labware_core.py @@ -5,6 +5,7 @@ from opentrons.protocols.api_support.tip_tracker import TipTracker from opentrons.types import DeckSlotName, Location, Point +from opentrons.hardware_control.nozzle_manager import NozzleMap from opentrons_shared_data.labware.dev_types import LabwareParameters, LabwareDefinition from ..labware import AbstractLabware, LabwareLoadParams @@ -153,8 +154,15 @@ def reset_tips(self) -> None: well.set_has_tip(True) def get_next_tip( - self, num_tips: int, starting_tip: Optional[LegacyWellCore] + self, + num_tips: int, + starting_tip: Optional[LegacyWellCore], + nozzle_map: Optional[NozzleMap], ) -> Optional[str]: + if nozzle_map is not None: + raise ValueError( + "Nozzle Map cannot be provided to calls for next tip in legacy protocols." + ) next_well = self._tip_tracker.next_tip(num_tips, starting_tip) return next_well.get_name() if next_well else None diff --git a/api/src/opentrons/protocol_api/core/legacy_simulator/legacy_instrument_core.py b/api/src/opentrons/protocol_api/core/legacy_simulator/legacy_instrument_core.py index fb47da62c50..2ee61adf24e 100644 --- a/api/src/opentrons/protocol_api/core/legacy_simulator/legacy_instrument_core.py +++ b/api/src/opentrons/protocol_api/core/legacy_simulator/legacy_instrument_core.py @@ -23,6 +23,7 @@ from ...disposal_locations import TrashBin, WasteChute from opentrons.protocol_api._nozzle_layout import NozzleLayout +from opentrons.hardware_control.nozzle_manager import NozzleMap from ..instrument import AbstractInstrument @@ -468,6 +469,10 @@ def get_active_channels(self) -> int: """This will never be called because it was added in API 2.16.""" assert False, "get_active_channels only supported in API 2.16 & later" + def get_nozzle_map(self) -> NozzleMap: + """This will never be called because it was added in API 2.18.""" + assert False, "get_nozzle_map only supported in API 2.18 & later" + def is_tip_tracking_available(self) -> bool: # Tip tracking is always available in legacy context return True diff --git a/api/src/opentrons/protocol_api/instrument_context.py b/api/src/opentrons/protocol_api/instrument_context.py index 56c8dd4b5eb..9754def8e5b 100644 --- a/api/src/opentrons/protocol_api/instrument_context.py +++ b/api/src/opentrons/protocol_api/instrument_context.py @@ -27,6 +27,7 @@ requires_version, APIVersionError, ) +from opentrons.hardware_control.nozzle_manager import NozzleConfigurationType from .core.common import InstrumentCore, ProtocolCore from .core.engine import ENGINE_CORE_API_VERSION @@ -56,6 +57,9 @@ _DROP_TIP_LOCATION_ALTERNATING_ADDED_IN = APIVersion(2, 15) """The version after which a drop-tip-into-trash procedure drops tips in different alternating locations within the trash well.""" _PARTIAL_NOZZLE_CONFIGURATION_ADDED_IN = APIVersion(2, 16) +"""The version after which a partial nozzle configuration became available for the 96 Channel Pipette.""" +_PARTIAL_NOZZLE_CONFIGURATION_AUTOMATIC_TIP_TRACKING_IN = APIVersion(2, 18) +"""The version after which automatic tip tracking supported partially configured nozzle layouts.""" class InstrumentContext(publisher.CommandPublisher): @@ -877,8 +881,31 @@ def pick_up_tip( # noqa: C901 if self._api_version >= _PARTIAL_NOZZLE_CONFIGURATION_ADDED_IN else self.channels ) + nozzle_map = ( + self._core.get_nozzle_map() + if self._api_version + >= _PARTIAL_NOZZLE_CONFIGURATION_AUTOMATIC_TIP_TRACKING_IN + else None + ) if location is None: + if ( + nozzle_map is not None + and nozzle_map.configuration != NozzleConfigurationType.FULL + and self.starting_tip is not None + ): + # Disallowing this avoids concerning the system with the direction + # in which self.starting_tip consumes tips. It would currently vary + # depending on the configuration layout of a pipette at a given + # time, which means that some combination of starting tip and partial + # configuraiton are incompatible under the current understanding of + # starting tip behavior. Replacing starting_tip with an undeprecated + # Labware.has_tip may solve this. + raise CommandPreconditionViolated( + "Automatic tip tracking is not available when using a partial pipette" + " nozzle configuration and InstrumentContext.starting_tip." + " Switch to a full configuration or set starting_tip to None." + ) if not self._core.is_tip_tracking_available(): raise CommandPreconditionViolated( "Automatic tip tracking is not available for the current pipette" @@ -886,11 +913,11 @@ def pick_up_tip( # noqa: C901 " that supports automatic tip tracking or specifying the exact tip" " to pick up." ) - tip_rack, well = labware.next_available_tip( starting_tip=self.starting_tip, tip_racks=self.tip_racks, channels=active_channels, + nozzle_map=nozzle_map, ) elif isinstance(location, labware.Well): @@ -902,6 +929,7 @@ def pick_up_tip( # noqa: C901 starting_tip=None, tip_racks=[location], channels=active_channels, + nozzle_map=nozzle_map, ) elif isinstance(location, types.Location): @@ -917,6 +945,7 @@ def pick_up_tip( # noqa: C901 starting_tip=None, tip_racks=[maybe_tip_rack], channels=active_channels, + nozzle_map=nozzle_map, ) else: raise TypeError( @@ -1323,6 +1352,12 @@ def transfer( # noqa: C901 if self._api_version >= _PARTIAL_NOZZLE_CONFIGURATION_ADDED_IN else self.channels ) + nozzle_map = ( + self._core.get_nozzle_map() + if self._api_version + >= _PARTIAL_NOZZLE_CONFIGURATION_AUTOMATIC_TIP_TRACKING_IN + else None + ) if blow_out and not blowout_location: if self.current_volume: @@ -1339,7 +1374,10 @@ def transfer( # noqa: C901 if new_tip != types.TransferTipPolicy.NEVER: tr, next_tip = labware.next_available_tip( - self.starting_tip, self.tip_racks, active_channels + self.starting_tip, + self.tip_racks, + active_channels, + nozzle_map=nozzle_map, ) max_volume = min(next_tip.max_volume, self.max_volume) else: diff --git a/api/src/opentrons/protocol_api/labware.py b/api/src/opentrons/protocol_api/labware.py index 9333c75f60d..ecb4d06ac5b 100644 --- a/api/src/opentrons/protocol_api/labware.py +++ b/api/src/opentrons/protocol_api/labware.py @@ -20,6 +20,7 @@ from opentrons.types import Location, Point from opentrons.protocols.api_support.types import APIVersion from opentrons.protocols.api_support.util import requires_version, APIVersionError +from opentrons.hardware_control.nozzle_manager import NozzleMap # TODO(mc, 2022-09-02): re-exports provided for backwards compatibility # remove when their usage is no longer needed @@ -883,7 +884,11 @@ def tip_length(self, length: float) -> None: # TODO(mc, 2022-11-09): implementation detail; deprecate public method def next_tip( - self, num_tips: int = 1, starting_tip: Optional[Well] = None + self, + num_tips: int = 1, + starting_tip: Optional[Well] = None, + *, + nozzle_map: Optional[NozzleMap] = None, ) -> Optional[Well]: """ Find the next valid well for pick-up. @@ -904,6 +909,7 @@ def next_tip( well_name = self._core.get_next_tip( num_tips=num_tips, starting_tip=starting_tip._core if starting_tip else None, + nozzle_map=nozzle_map, ) return self._wells_by_name[well_name] if well_name is not None else None @@ -1061,7 +1067,11 @@ def split_tipracks(tip_racks: List[Labware]) -> Tuple[Labware, List[Labware]]: # TODO(mc, 2022-11-09): implementation detail, move to core def select_tiprack_from_list( - tip_racks: List[Labware], num_channels: int, starting_point: Optional[Well] = None + tip_racks: List[Labware], + num_channels: int, + starting_point: Optional[Well] = None, + *, + nozzle_map: Optional[NozzleMap] = None, ) -> Tuple[Labware, Well]: try: first, rest = split_tipracks(tip_racks) @@ -1074,14 +1084,16 @@ def select_tiprack_from_list( ) elif starting_point: first_well = starting_point + elif nozzle_map: + first_well = None else: first_well = first.wells()[0] - next_tip = first.next_tip(num_channels, first_well) + next_tip = first.next_tip(num_channels, first_well, nozzle_map=nozzle_map) if next_tip: return first, next_tip else: - return select_tiprack_from_list(rest, num_channels) + return select_tiprack_from_list(rest, num_channels, None, nozzle_map=nozzle_map) # TODO(mc, 2022-11-09): implementation detail, move to core @@ -1093,14 +1105,23 @@ def filter_tipracks_to_start( # TODO(mc, 2022-11-09): implementation detail, move to core def next_available_tip( - starting_tip: Optional[Well], tip_racks: List[Labware], channels: int + starting_tip: Optional[Well], + tip_racks: List[Labware], + channels: int, + *, + nozzle_map: Optional[NozzleMap] = None, ) -> Tuple[Labware, Well]: start = starting_tip if start is None: - return select_tiprack_from_list(tip_racks, channels) + return select_tiprack_from_list( + tip_racks, channels, None, nozzle_map=nozzle_map + ) else: return select_tiprack_from_list( - filter_tipracks_to_start(start, tip_racks), channels, start + filter_tipracks_to_start(start, tip_racks), + channels, + start, + nozzle_map=nozzle_map, ) diff --git a/api/src/opentrons/protocol_engine/__init__.py b/api/src/opentrons/protocol_engine/__init__.py index f6737a71432..07f2ae17f9c 100644 --- a/api/src/opentrons/protocol_engine/__init__.py +++ b/api/src/opentrons/protocol_engine/__init__.py @@ -20,6 +20,7 @@ CommandStatus, CommandType, CommandIntent, + CommandNote, ) from .state import State, StateView, StateSummary, CommandSlice, CurrentCommand, Config from .plugins import AbstractPlugin @@ -79,6 +80,7 @@ "CommandStatus", "CommandType", "CommandIntent", + "CommandNote", # state interfaces and models "State", "StateView", diff --git a/api/src/opentrons/protocol_engine/commands/__init__.py b/api/src/opentrons/protocol_engine/commands/__init__.py index 3dfe6eaf51f..97f0744a9a2 100644 --- a/api/src/opentrons/protocol_engine/commands/__init__.py +++ b/api/src/opentrons/protocol_engine/commands/__init__.py @@ -28,6 +28,7 @@ BaseCommandCreate, CommandStatus, CommandIntent, + CommandNote, ) from .command_unions import ( @@ -332,6 +333,7 @@ "BaseCommandCreate", "CommandStatus", "CommandIntent", + "CommandNote", # command parameter hashing "hash_command_params", # command schema generation diff --git a/api/src/opentrons/protocol_engine/commands/command.py b/api/src/opentrons/protocol_engine/commands/command.py index f8f48bba67c..1bf72e12352 100644 --- a/api/src/opentrons/protocol_engine/commands/command.py +++ b/api/src/opentrons/protocol_engine/commands/command.py @@ -6,7 +6,16 @@ from abc import ABC, abstractmethod from datetime import datetime from enum import Enum -from typing import TYPE_CHECKING, Generic, Optional, TypeVar, Tuple +from typing import ( + TYPE_CHECKING, + Generic, + Optional, + TypeVar, + Tuple, + Union, + Literal, + List, +) from pydantic import BaseModel, Field from pydantic.generics import GenericModel @@ -27,6 +36,29 @@ CommandPrivateResultT = TypeVar("CommandPrivateResultT") +NoteKind = Union[Literal["warning", "information"], str] + + +class CommandNote(BaseModel): + """A note about a command's execution or dispatch.""" + + noteKind: NoteKind = Field( + ..., + description="The kind of note this is. Only the literal possibilities should be" + " relied upon programmatically.", + ) + shortMessage: str = Field( + ..., + description="The accompanying human-readable short message (suitable for display in a single line)", + ) + longMessage: str = Field( + ..., + description="A longer message that may contain newlines and formatting characters describing the note.", + ) + source: str = Field( + ..., description="An identifier for the party that created the note" + ) + class CommandStatus(str, Enum): """Command execution status.""" @@ -144,6 +176,13 @@ class BaseCommand(GenericModel, Generic[CommandParamsT, CommandResultT]): " a command that is part of a calibration procedure." ), ) + notes: Optional[List[CommandNote]] = Field( + None, + description=( + "Information not critical to the execution of the command derived from either" + " the command's execution or the command's generation." + ), + ) class AbstractCommandImpl( diff --git a/api/src/opentrons/protocol_engine/state/tips.py b/api/src/opentrons/protocol_engine/state/tips.py index 0e68710ae28..67598c32bba 100644 --- a/api/src/opentrons/protocol_engine/state/tips.py +++ b/api/src/opentrons/protocol_engine/state/tips.py @@ -1,7 +1,7 @@ """Tip state tracking.""" from dataclasses import dataclass from enum import Enum -from typing import Dict, Optional, List +from typing import Dict, Optional, List, Union from .abstract_store import HasState, HandlesActions from ..actions import ( @@ -21,6 +21,8 @@ PipetteNozzleLayoutResultMixin, ) +from opentrons.hardware_control.nozzle_manager import NozzleMap + class TipRackWellState(Enum): """The state of a single tip in a tip rack's well.""" @@ -41,6 +43,7 @@ class TipState: channels_by_pipette_id: Dict[str, int] length_by_pipette_id: Dict[str, float] active_channels_by_pipette_id: Dict[str, int] + nozzle_map_by_pipette_id: Dict[str, NozzleMap] class TipStore(HasState[TipState], HandlesActions): @@ -56,6 +59,7 @@ def __init__(self) -> None: channels_by_pipette_id={}, length_by_pipette_id={}, active_channels_by_pipette_id={}, + nozzle_map_by_pipette_id={}, ) def handle_action(self, action: Action) -> None: @@ -66,6 +70,7 @@ def handle_action(self, action: Action) -> None: config = action.private_result.config self._state.channels_by_pipette_id[pipette_id] = config.channels self._state.active_channels_by_pipette_id[pipette_id] = config.channels + self._state.nozzle_map_by_pipette_id[pipette_id] = config.nozzle_map self._handle_command(action.command) if isinstance(action.private_result, PipetteNozzleLayoutResultMixin): @@ -75,6 +80,7 @@ def handle_action(self, action: Action) -> None: self._state.active_channels_by_pipette_id[ pipette_id ] = nozzle_map.tip_count + self._state.nozzle_map_by_pipette_id[pipette_id] = nozzle_map else: self._state.active_channels_by_pipette_id[ pipette_id @@ -118,24 +124,46 @@ def _handle_command(self, command: Command) -> None: pipette_id = command.params.pipetteId self._state.length_by_pipette_id.pop(pipette_id, None) - def _set_used_tips(self, pipette_id: str, well_name: str, labware_id: str) -> None: - pipette_channels = self._state.active_channels_by_pipette_id.get(pipette_id) + def _set_used_tips( # noqa: C901 + self, pipette_id: str, well_name: str, labware_id: str + ) -> None: columns = self._state.column_by_labware_id.get(labware_id, []) wells = self._state.tips_by_labware_id.get(labware_id, {}) - - if pipette_channels == len(wells): - for well_name in wells.keys(): - wells[well_name] = TipRackWellState.USED - - elif columns and pipette_channels == len(columns[0]): - for column in columns: - if well_name in column: - for well in column: + nozzle_map = self._state.nozzle_map_by_pipette_id[pipette_id] + + # TODO (cb, 02-28-2024): Transition from using partial nozzle map to full instrument map for the set used logic + num_nozzle_cols = len(nozzle_map.columns) + num_nozzle_rows = len(nozzle_map.rows) + + critical_column = 0 + critical_row = 0 + for column in columns: + if well_name in column: + critical_row = column.index(well_name) + critical_column = columns.index(column) + + for i in range(num_nozzle_cols): + for j in range(num_nozzle_rows): + if nozzle_map.starting_nozzle == "A1": + if (critical_column + i < len(columns)) and ( + critical_row + j < len(columns[critical_column]) + ): + well = columns[critical_column + i][critical_row + j] + wells[well] = TipRackWellState.USED + elif nozzle_map.starting_nozzle == "A12": + if (critical_column - i >= 0) and ( + critical_row + j < len(columns[critical_column]) + ): + well = columns[critical_column - i][critical_row + j] + wells[well] = TipRackWellState.USED + elif nozzle_map.starting_nozzle == "H1": + if (critical_column + i < len(columns)) and (critical_row - j >= 0): + well = columns[critical_column + i][critical_row - j] + wells[well] = TipRackWellState.USED + elif nozzle_map.starting_nozzle == "H12": + if (critical_column - i >= 0) and (critical_row - j >= 0): + well = columns[critical_column - i][critical_row - j] wells[well] = TipRackWellState.USED - break - - else: - wells[well_name] = TipRackWellState.USED class TipView(HasState[TipState]): @@ -151,50 +179,255 @@ def __init__(self, state: TipState) -> None: """ self._state = state - # TODO (spp, 2023-12-05): update this logic once we support partial nozzle configurations - # that require the tip tracking to move right to left or front to back; - # for example when using leftmost column config of 96-channel - # or backmost single nozzle configuration of an 8-channel. def get_next_tip( # noqa: C901 - self, labware_id: str, num_tips: int, starting_tip_name: Optional[str] + self, + labware_id: str, + num_tips: int, + starting_tip_name: Optional[str], + nozzle_map: Optional[NozzleMap], ) -> Optional[str]: - """Get the next available clean tip.""" + """Get the next available clean tip. Does not support use of a starting tip if the pipette used is in a partial configuration.""" wells = self._state.tips_by_labware_id.get(labware_id, {}) columns = self._state.column_by_labware_id.get(labware_id, []) - if columns and num_tips == len(columns[0]): # Get next tips for 8-channel - column_head = [column[0] for column in columns] - starting_column_index = 0 - - if starting_tip_name: - for idx, column in enumerate(columns): - if starting_tip_name in column: - if starting_tip_name not in column_head: - starting_column_index = idx + 1 + def _identify_tip_cluster( + active_columns: int, + active_rows: int, + critical_column: int, + critical_row: int, + entry_well: str, + ) -> Optional[List[str]]: + tip_cluster = [] + for i in range(active_columns): + if entry_well == "A1" or entry_well == "H1": + if critical_column - i >= 0: + column = columns[critical_column - i] + else: + return None + elif entry_well == "A12" or entry_well == "H12": + if critical_column + i < len(columns): + column = columns[critical_column + i] + else: + return None + else: + raise ValueError( + f"Invalid entry well {entry_well} for tip cluster identification." + ) + for j in range(active_rows): + if entry_well == "A1" or entry_well == "A12": + if critical_row - j >= 0: + well = column[critical_row - j] else: - starting_column_index = idx - - for column in columns[starting_column_index:]: - if not any(wells[well] == TipRackWellState.USED for well in column): - return column[0] + return None + elif entry_well == "H1" or entry_well == "H12": + if critical_row + j < len(column): + well = column[critical_row + j] + else: + return None + tip_cluster.append(well) - elif num_tips == len(wells.keys()): # Get next tips for 96 channel - if starting_tip_name and starting_tip_name != columns[0][0]: + if any(well not in [*wells] for well in tip_cluster): return None - if not any( - tip_state == TipRackWellState.USED for tip_state in wells.values() - ): - return next(iter(wells)) - - else: # Get next tips for single channel - if starting_tip_name is not None: - wells = _drop_wells_before_starting_tip(wells, starting_tip_name) - - for well_name, tip_state in wells.items(): - if tip_state == TipRackWellState.CLEAN: - return well_name + return tip_cluster + def _validate_tip_cluster( + active_columns: int, active_rows: int, tip_cluster: List[str] + ) -> Union[str, int, None]: + if not any(wells[well] == TipRackWellState.USED for well in tip_cluster): + return tip_cluster[0] + elif all(wells[well] == TipRackWellState.USED for well in tip_cluster): + return None + else: + # The tip cluster list is ordered: Each row from a column in order by columns + tip_cluster_final_column = [] + for i in range(active_rows): + tip_cluster_final_column.append( + tip_cluster[((active_columns * active_rows) - 1) - i] + ) + tip_cluster_final_row = [] + for i in range(active_columns): + tip_cluster_final_row.append( + tip_cluster[(active_rows - 1) + (i * active_rows)] + ) + if all( + wells[well] == TipRackWellState.USED + for well in tip_cluster_final_column + ): + return None + elif all( + wells[well] == TipRackWellState.USED + for well in tip_cluster_final_row + ): + return None + else: + # Tiprack has no valid tip selection, cannot progress + return -1 + + # Search through the tiprack beginning at A1 + def _cluster_search_A1(active_columns: int, active_rows: int) -> Optional[str]: + critical_column = active_columns - 1 + critical_row = active_rows - 1 + + while critical_column <= len(columns): + tip_cluster = _identify_tip_cluster( + active_columns, active_rows, critical_column, critical_row, "A1" + ) + if tip_cluster is not None: + result = _validate_tip_cluster( + active_columns, active_rows, tip_cluster + ) + if isinstance(result, str): + return result + elif isinstance(result, int) and result == -1: + return None + if critical_row + active_rows < len(columns[0]): + critical_row = critical_row + active_rows + else: + critical_column = critical_column + 1 + critical_row = active_rows - 1 + return None + + # Search through the tiprack beginning at A12 + def _cluster_search_A12(active_columns: int, active_rows: int) -> Optional[str]: + critical_column = len(columns) - active_columns + critical_row = active_rows - 1 + + while critical_column >= 0: + tip_cluster = _identify_tip_cluster( + active_columns, active_rows, critical_column, critical_row, "A12" + ) + if tip_cluster is not None: + result = _validate_tip_cluster( + active_columns, active_rows, tip_cluster + ) + if isinstance(result, str): + return result + elif isinstance(result, int) and result == -1: + return None + if critical_row + active_rows < len(columns[0]): + critical_row = critical_row + active_rows + else: + critical_column = critical_column - 1 + critical_row = active_rows - 1 + return None + + # Search through the tiprack beginning at H1 + def _cluster_search_H1(active_columns: int, active_rows: int) -> Optional[str]: + critical_column = active_columns - 1 + critical_row = len(columns[critical_column]) - active_rows + + while critical_column <= len(columns): # change to max size of labware + tip_cluster = _identify_tip_cluster( + active_columns, active_rows, critical_column, critical_row, "H1" + ) + if tip_cluster is not None: + result = _validate_tip_cluster( + active_columns, active_rows, tip_cluster + ) + if isinstance(result, str): + return result + elif isinstance(result, int) and result == -1: + return None + if critical_row - active_rows >= 0: + critical_row = critical_row - active_rows + else: + critical_column = critical_column + 1 + critical_row = len(columns[critical_column]) - active_rows + return None + + # Search through the tiprack beginning at H12 + def _cluster_search_H12(active_columns: int, active_rows: int) -> Optional[str]: + critical_column = len(columns) - active_columns + critical_row = len(columns[critical_column]) - active_rows + + while critical_column >= 0: + tip_cluster = _identify_tip_cluster( + active_columns, active_rows, critical_column, critical_row, "H12" + ) + if tip_cluster is not None: + result = _validate_tip_cluster( + active_columns, active_rows, tip_cluster + ) + if isinstance(result, str): + return result + elif isinstance(result, int) and result == -1: + return None + if critical_row - active_rows >= 0: + critical_row = critical_row - active_rows + else: + critical_column = critical_column - 1 + critical_row = len(columns[critical_column]) - active_rows + return None + + if starting_tip_name is None and nozzle_map is not None and columns: + num_channels = len(nozzle_map.full_instrument_map_store) + num_nozzle_cols = len(nozzle_map.columns) + num_nozzle_rows = len(nozzle_map.rows) + # Each pipette's cluster search is determined by the point of entry for a given pipette/configuration: + # - Single channel pipettes always search a tiprack top to bottom, left to right + # - Eight channel pipettes will begin at the top if the primary nozzle is H1 and at the bottom if + # it is A1. The eight channel will always progress across the columns left to right. + # - 96 Channel pipettes will begin in the corner opposite their primary/starting nozzle (if starting nozzle = A1, enter tiprack at H12) + # The 96 channel will then progress towards the opposite corner, either going up or down, left or right depending on configuration. + + if num_channels == 1: + return _cluster_search_A1(num_nozzle_cols, num_nozzle_rows) + elif num_channels == 8: + if nozzle_map.starting_nozzle == "A1": + return _cluster_search_H1(num_nozzle_cols, num_nozzle_rows) + elif nozzle_map.starting_nozzle == "H1": + return _cluster_search_A1(num_nozzle_cols, num_nozzle_rows) + elif num_channels == 96: + if nozzle_map.starting_nozzle == "A1": + return _cluster_search_H12(num_nozzle_cols, num_nozzle_rows) + elif nozzle_map.starting_nozzle == "A12": + return _cluster_search_H1(num_nozzle_cols, num_nozzle_rows) + elif nozzle_map.starting_nozzle == "H1": + return _cluster_search_A12(num_nozzle_cols, num_nozzle_rows) + elif nozzle_map.starting_nozzle == "H12": + return _cluster_search_A1(num_nozzle_cols, num_nozzle_rows) + else: + raise ValueError( + f"Nozzle {nozzle_map.starting_nozzle} is an invalid starting tip for automatic tip pickup." + ) + else: + raise RuntimeError( + "Invalid number of channels for automatic tip tracking." + ) + else: + if columns and num_tips == len(columns[0]): # Get next tips for 8-channel + column_head = [column[0] for column in columns] + starting_column_index = 0 + + if starting_tip_name: + for idx, column in enumerate(columns): + if starting_tip_name in column: + if starting_tip_name not in column_head: + starting_column_index = idx + 1 + else: + starting_column_index = idx + + for column in columns[starting_column_index:]: + if not any(wells[well] == TipRackWellState.USED for well in column): + return column[0] + + elif num_tips == len(wells.keys()): # Get next tips for 96 channel + if starting_tip_name and starting_tip_name != columns[0][0]: + return None + + if not any( + tip_state == TipRackWellState.USED for tip_state in wells.values() + ): + return next(iter(wells)) + + else: # Get next tips for single channel + if starting_tip_name is not None: + wells = _drop_wells_before_starting_tip(wells, starting_tip_name) + + for well_name, tip_state in wells.items(): + if tip_state == TipRackWellState.CLEAN: + return well_name return None def get_pipette_channels(self, pipette_id: str) -> int: @@ -205,6 +438,10 @@ def get_pipette_active_channels(self, pipette_id: str) -> int: """Get the number of channels being used in the given pipette's configuration.""" return self._state.active_channels_by_pipette_id[pipette_id] + def get_pipette_nozzle_map(self, pipette_id: str) -> NozzleMap: + """Get the current nozzle map the given pipette's configuration.""" + return self._state.nozzle_map_by_pipette_id[pipette_id] + def has_clean_tip(self, labware_id: str, well_name: str) -> bool: """Get whether a well in a labware has a clean tip. diff --git a/api/src/opentrons/protocol_engine/types.py b/api/src/opentrons/protocol_engine/types.py index 656f2263efc..9494ae3eec1 100644 --- a/api/src/opentrons/protocol_engine/types.py +++ b/api/src/opentrons/protocol_engine/types.py @@ -748,7 +748,7 @@ class PostRunHardwareState(Enum): DISENGAGE_IN_PLACE = "disengageInPlace" -NOZZLE_NAME_REGEX = "[A-Z][0-100]" +NOZZLE_NAME_REGEX = r"[A-Z]\d{1,2}" PRIMARY_NOZZLE_LITERAL = Literal["A1", "H1", "A12", "H12"] diff --git a/api/tests/opentrons/protocol_api/core/engine/test_instrument_core.py b/api/tests/opentrons/protocol_api/core/engine/test_instrument_core.py index 0b5a0f26a47..3b296067a0d 100644 --- a/api/tests/opentrons/protocol_api/core/engine/test_instrument_core.py +++ b/api/tests/opentrons/protocol_api/core/engine/test_instrument_core.py @@ -1139,11 +1139,11 @@ def test_configure_nozzle_layout( argvalues=[ (96, NozzleConfigurationType.FULL, "A1", True), (96, NozzleConfigurationType.FULL, None, True), - (96, NozzleConfigurationType.ROW, "A1", False), - (96, NozzleConfigurationType.COLUMN, "A1", False), + (96, NozzleConfigurationType.ROW, "A1", True), + (96, NozzleConfigurationType.COLUMN, "A1", True), (96, NozzleConfigurationType.COLUMN, "A12", True), - (96, NozzleConfigurationType.SINGLE, "H12", False), - (96, NozzleConfigurationType.SINGLE, "A1", False), + (96, NozzleConfigurationType.SINGLE, "H12", True), + (96, NozzleConfigurationType.SINGLE, "A1", True), (8, NozzleConfigurationType.FULL, "A1", True), (8, NozzleConfigurationType.FULL, None, True), (8, NozzleConfigurationType.SINGLE, "H1", True), diff --git a/api/tests/opentrons/protocol_api/core/engine/test_labware_core.py b/api/tests/opentrons/protocol_api/core/engine/test_labware_core.py index 5f84df6f62c..37d4511cce0 100644 --- a/api/tests/opentrons/protocol_api/core/engine/test_labware_core.py +++ b/api/tests/opentrons/protocol_api/core/engine/test_labware_core.py @@ -249,13 +249,16 @@ def test_get_next_tip( labware_id="cool-labware", num_tips=8, starting_tip_name="B1", + nozzle_map=None, ) ).then_return("A2") starting_tip = WellCore( name="B1", labware_id="cool-labware", engine_client=mock_engine_client ) - result = subject.get_next_tip(num_tips=8, starting_tip=starting_tip) + result = subject.get_next_tip( + num_tips=8, starting_tip=starting_tip, nozzle_map=None + ) assert result == "A2" diff --git a/api/tests/opentrons/protocol_api/test_instrument_context.py b/api/tests/opentrons/protocol_api/test_instrument_context.py index 239d61c9d95..38ab8f5b54b 100644 --- a/api/tests/opentrons/protocol_api/test_instrument_context.py +++ b/api/tests/opentrons/protocol_api/test_instrument_context.py @@ -1,4 +1,5 @@ """Tests for the InstrumentContext public interface.""" +from collections import OrderedDict import inspect import pytest @@ -29,6 +30,8 @@ from opentrons.protocol_api.core.legacy.legacy_instrument_core import ( LegacyInstrumentCore, ) + +from opentrons.hardware_control.nozzle_manager import NozzleMap from opentrons.protocol_api.disposal_locations import TrashBin, WasteChute from opentrons.protocol_api._nozzle_layout import NozzleLayout from opentrons.types import Location, Mount, Point @@ -505,8 +508,25 @@ def test_blow_out_raises_no_location( subject.blow_out(location=None) +MOCK_MAP = NozzleMap.build( + physical_nozzles=OrderedDict({"A1": Point(0, 0, 0)}), + physical_rows=OrderedDict({"A": ["A1"]}), + physical_columns=OrderedDict({"1": ["A1"]}), + starting_nozzle="A1", + back_left_nozzle="A1", + front_right_nozzle="A1", +) + + +@pytest.mark.parametrize( + argnames=["api_version", "mock_map"], + argvalues=[(APIVersion(2, 18), MOCK_MAP), (APIVersion(2, 17), None)], +) def test_pick_up_tip_from_labware( - decoy: Decoy, mock_instrument_core: InstrumentCore, subject: InstrumentContext + decoy: Decoy, + mock_instrument_core: InstrumentCore, + subject: InstrumentContext, + mock_map: Optional[NozzleMap], ) -> None: """It should pick up the next tip from a given labware.""" mock_tip_rack = decoy.mock(cls=Labware) @@ -514,11 +534,13 @@ def test_pick_up_tip_from_labware( top_location = Location(point=Point(1, 2, 3), labware=mock_well) decoy.when(mock_instrument_core.get_active_channels()).then_return(123) + decoy.when(mock_instrument_core.get_nozzle_map()).then_return(MOCK_MAP) decoy.when( labware.next_available_tip( starting_tip=None, tip_racks=[mock_tip_rack], channels=123, + nozzle_map=mock_map, ) ).then_return((mock_tip_rack, mock_well)) decoy.when(mock_well.top()).then_return(top_location) @@ -558,8 +580,15 @@ def test_pick_up_tip_from_well_location( ) +@pytest.mark.parametrize( + argnames=["api_version", "mock_map"], + argvalues=[(APIVersion(2, 18), MOCK_MAP), (APIVersion(2, 17), None)], +) def test_pick_up_tip_from_labware_location( - decoy: Decoy, mock_instrument_core: InstrumentCore, subject: InstrumentContext + decoy: Decoy, + mock_instrument_core: InstrumentCore, + subject: InstrumentContext, + mock_map: Optional[NozzleMap], ) -> None: """It should pick up the next tip from a given labware-based Location.""" mock_tip_rack = decoy.mock(cls=Labware) @@ -568,11 +597,13 @@ def test_pick_up_tip_from_labware_location( top_location = Location(point=Point(1, 2, 3), labware=mock_well) decoy.when(mock_instrument_core.get_active_channels()).then_return(123) + decoy.when(mock_instrument_core.get_nozzle_map()).then_return(MOCK_MAP) decoy.when( labware.next_available_tip( starting_tip=None, tip_racks=[mock_tip_rack], channels=123, + nozzle_map=mock_map, ) ).then_return((mock_tip_rack, mock_well)) decoy.when(mock_well.top()).then_return(top_location) @@ -591,10 +622,17 @@ def test_pick_up_tip_from_labware_location( ) +@pytest.mark.parametrize( + argnames=["api_version", "mock_map"], + argvalues=[(APIVersion(2, 18), MOCK_MAP), (APIVersion(2, 17), None)], +) def test_pick_up_from_associated_tip_racks( - decoy: Decoy, mock_instrument_core: InstrumentCore, subject: InstrumentContext + decoy: Decoy, + mock_instrument_core: InstrumentCore, + subject: InstrumentContext, + mock_map: Optional[NozzleMap], ) -> None: - """It should pick up from it associated tip racks.""" + """It should pick up from its associated tip racks.""" mock_tip_rack_1 = decoy.mock(cls=Labware) mock_tip_rack_2 = decoy.mock(cls=Labware) mock_starting_tip = decoy.mock(cls=Well) @@ -603,11 +641,13 @@ def test_pick_up_from_associated_tip_racks( decoy.when(mock_instrument_core.is_tip_tracking_available()).then_return(True) decoy.when(mock_instrument_core.get_active_channels()).then_return(123) + decoy.when(mock_instrument_core.get_nozzle_map()).then_return(MOCK_MAP) decoy.when( labware.next_available_tip( starting_tip=mock_starting_tip, tip_racks=[mock_tip_rack_1, mock_tip_rack_2], channels=123, + nozzle_map=mock_map, ) ).then_return((mock_tip_rack_2, mock_well)) decoy.when(mock_well.top()).then_return(top_location) diff --git a/api/tests/opentrons/protocol_api_old/test_labware.py b/api/tests/opentrons/protocol_api_old/test_labware.py index c72c8a87346..8f6f1da267b 100644 --- a/api/tests/opentrons/protocol_api_old/test_labware.py +++ b/api/tests/opentrons/protocol_api_old/test_labware.py @@ -544,7 +544,10 @@ def test_tiprack_list(): core_map=None, # type: ignore[arg-type] ) - assert labware.select_tiprack_from_list([tiprack], 1) == (tiprack, tiprack["A1"]) + assert labware.select_tiprack_from_list([tiprack], 1) == ( + tiprack, + tiprack["A1"], + ) assert labware.select_tiprack_from_list([tiprack], 1, tiprack.wells()[1]) == ( tiprack, diff --git a/api/tests/opentrons/protocol_engine/pipette_fixtures.py b/api/tests/opentrons/protocol_engine/pipette_fixtures.py index 26c2ed33448..70937beeb9f 100644 --- a/api/tests/opentrons/protocol_engine/pipette_fixtures.py +++ b/api/tests/opentrons/protocol_engine/pipette_fixtures.py @@ -331,7 +331,7 @@ def get_default_nozzle_map(pipette_type: PipetteNameType) -> NozzleMap: physical_columns=EIGHT_CHANNEL_COLS, starting_nozzle="A1", back_left_nozzle="A1", - front_right_nozzle="A1", + front_right_nozzle="H1", ) elif "96" in pipette_type.value: return NozzleMap.build( @@ -340,7 +340,7 @@ def get_default_nozzle_map(pipette_type: PipetteNameType) -> NozzleMap: physical_columns=NINETY_SIX_COLS, starting_nozzle="A1", back_left_nozzle="A1", - front_right_nozzle="A1", + front_right_nozzle="H12", ) else: return NozzleMap.build( diff --git a/api/tests/opentrons/protocol_engine/state/test_tip_state.py b/api/tests/opentrons/protocol_engine/state/test_tip_state.py index a164656aeca..3f4ff0cf860 100644 --- a/api/tests/opentrons/protocol_engine/state/test_tip_state.py +++ b/api/tests/opentrons/protocol_engine/state/test_tip_state.py @@ -115,17 +115,51 @@ def drop_tip_in_place_command() -> commands.DropTipInPlace: ], ) def test_get_next_tip_returns_none( - load_labware_command: commands.LoadLabware, subject: TipStore + load_labware_command: commands.LoadLabware, + subject: TipStore, + supported_tip_fixture: pipette_definition.SupportedTipsDefinition, ) -> None: """It should start at the first tip in the labware.""" subject.handle_action( actions.UpdateCommandAction(private_result=None, command=load_labware_command) ) + load_pipette_command = commands.LoadPipette.construct( # type: ignore[call-arg] + result=commands.LoadPipetteResult(pipetteId="pipette-id") + ) + load_pipette_private_result = commands.LoadPipettePrivateResult( + pipette_id="pipette-id", + serial_number="pipette-serial", + config=LoadedStaticPipetteData( + channels=96, + max_volume=15, + min_volume=3, + model="gen a", + display_name="display name", + flow_rates=FlowRates( + default_aspirate={}, + default_dispense={}, + default_blow_out={}, + ), + tip_configuration_lookup_table={15: supported_tip_fixture}, + nominal_tip_overlap={}, + nozzle_offset_z=1.23, + home_position=4.56, + nozzle_map=get_default_nozzle_map(PipetteNameType.P1000_96), + back_left_corner_offset=Point(0, 0, 0), + front_right_corner_offset=Point(0, 0, 0), + ), + ) + subject.handle_action( + actions.UpdateCommandAction( + private_result=load_pipette_private_result, command=load_pipette_command + ) + ) result = TipView(subject.state).get_next_tip( labware_id="cool-labware", num_tips=1, starting_tip_name=None, + nozzle_map=None, ) assert result is None @@ -133,17 +167,59 @@ def test_get_next_tip_returns_none( @pytest.mark.parametrize("input_tip_amount", [1, 8, 96]) def test_get_next_tip_returns_first_tip( - load_labware_command: commands.LoadLabware, subject: TipStore, input_tip_amount: int + load_labware_command: commands.LoadLabware, + subject: TipStore, + input_tip_amount: int, + supported_tip_fixture: pipette_definition.SupportedTipsDefinition, ) -> None: """It should start at the first tip in the labware.""" subject.handle_action( actions.UpdateCommandAction(private_result=None, command=load_labware_command) ) + load_pipette_command = commands.LoadPipette.construct( # type: ignore[call-arg] + result=commands.LoadPipetteResult(pipetteId="pipette-id") + ) + pipette_name_type = PipetteNameType.P1000_96 + if input_tip_amount == 1: + pipette_name_type = PipetteNameType.P300_SINGLE_GEN2 + elif input_tip_amount == 8: + pipette_name_type = PipetteNameType.P300_MULTI_GEN2 + else: + pipette_name_type = PipetteNameType.P1000_96 + load_pipette_private_result = commands.LoadPipettePrivateResult( + pipette_id="pipette-id", + serial_number="pipette-serial", + config=LoadedStaticPipetteData( + channels=input_tip_amount, + max_volume=15, + min_volume=3, + model="gen a", + display_name="display name", + flow_rates=FlowRates( + default_aspirate={}, + default_dispense={}, + default_blow_out={}, + ), + tip_configuration_lookup_table={15: supported_tip_fixture}, + nominal_tip_overlap={}, + nozzle_offset_z=1.23, + home_position=4.56, + nozzle_map=get_default_nozzle_map(pipette_name_type), + back_left_corner_offset=Point(0, 0, 0), + front_right_corner_offset=Point(0, 0, 0), + ), + ) + subject.handle_action( + actions.UpdateCommandAction( + private_result=load_pipette_private_result, command=load_pipette_command + ) + ) result = TipView(subject.state).get_next_tip( labware_id="cool-labware", num_tips=input_tip_amount, starting_tip_name=None, + nozzle_map=None, ) assert result == "A1" @@ -155,16 +231,49 @@ def test_get_next_tip_used_starting_tip( subject: TipStore, input_tip_amount: int, result_well_name: str, + supported_tip_fixture: pipette_definition.SupportedTipsDefinition, ) -> None: """It should start searching at the given starting tip.""" subject.handle_action( actions.UpdateCommandAction(private_result=None, command=load_labware_command) ) + load_pipette_command = commands.LoadPipette.construct( # type: ignore[call-arg] + result=commands.LoadPipetteResult(pipetteId="pipette-id") + ) + load_pipette_private_result = commands.LoadPipettePrivateResult( + pipette_id="pipette-id", + serial_number="pipette-serial", + config=LoadedStaticPipetteData( + channels=input_tip_amount, + max_volume=15, + min_volume=3, + model="gen a", + display_name="display name", + flow_rates=FlowRates( + default_aspirate={}, + default_dispense={}, + default_blow_out={}, + ), + tip_configuration_lookup_table={15: supported_tip_fixture}, + nominal_tip_overlap={}, + nozzle_offset_z=1.23, + home_position=4.56, + nozzle_map=get_default_nozzle_map(PipetteNameType.P300_SINGLE_GEN2), + back_left_corner_offset=Point(0, 0, 0), + front_right_corner_offset=Point(0, 0, 0), + ), + ) + subject.handle_action( + actions.UpdateCommandAction( + private_result=load_pipette_private_result, command=load_pipette_command + ) + ) result = TipView(subject.state).get_next_tip( labware_id="cool-labware", num_tips=input_tip_amount, starting_tip_name="B1", + nozzle_map=None, ) assert result == result_well_name @@ -201,11 +310,29 @@ def test_get_next_tip_skips_picked_up_tip( load_pipette_command = commands.LoadPipette.construct( # type: ignore[call-arg] result=commands.LoadPipetteResult(pipetteId="pipette-id") ) + channels_num = input_tip_amount + if input_starting_tip is not None: + pipette_name_type = PipetteNameType.P1000_96 + if input_tip_amount == 1: + pipette_name_type = PipetteNameType.P300_SINGLE_GEN2 + elif input_tip_amount == 8: + pipette_name_type = PipetteNameType.P300_MULTI_GEN2 + else: + pipette_name_type = PipetteNameType.P1000_96 + else: + channels_num = get_next_tip_tips + pipette_name_type = PipetteNameType.P1000_96 + if get_next_tip_tips == 1: + pipette_name_type = PipetteNameType.P300_SINGLE_GEN2 + elif get_next_tip_tips == 8: + pipette_name_type = PipetteNameType.P300_MULTI_GEN2 + else: + pipette_name_type = PipetteNameType.P1000_96 load_pipette_private_result = commands.LoadPipettePrivateResult( pipette_id="pipette-id", serial_number="pipette-serial", config=LoadedStaticPipetteData( - channels=input_tip_amount, + channels=channels_num, max_volume=15, min_volume=3, model="gen a", @@ -219,9 +346,9 @@ def test_get_next_tip_skips_picked_up_tip( nominal_tip_overlap={}, nozzle_offset_z=1.23, home_position=4.56, - nozzle_map=get_default_nozzle_map(PipetteNameType.P300_SINGLE_GEN2), - back_left_corner_offset=Point(x=1, y=2, z=3), - front_right_corner_offset=Point(x=4, y=5, z=6), + nozzle_map=get_default_nozzle_map(pipette_name_type), + back_left_corner_offset=Point(0, 0, 0), + front_right_corner_offset=Point(0, 0, 0), ), ) subject.handle_action( @@ -237,6 +364,7 @@ def test_get_next_tip_skips_picked_up_tip( labware_id="cool-labware", num_tips=get_next_tip_tips, starting_tip_name=input_starting_tip, + nozzle_map=load_pipette_private_result.config.nozzle_map, ) assert result == result_well_name @@ -245,16 +373,48 @@ def test_get_next_tip_skips_picked_up_tip( def test_get_next_tip_with_starting_tip( subject: TipStore, load_labware_command: commands.LoadLabware, + supported_tip_fixture: pipette_definition.SupportedTipsDefinition, ) -> None: """It should return the starting tip, and then the following tip after that.""" subject.handle_action( actions.UpdateCommandAction(private_result=None, command=load_labware_command) ) - + load_pipette_command = commands.LoadPipette.construct( # type: ignore[call-arg] + result=commands.LoadPipetteResult(pipetteId="pipette-id") + ) + load_pipette_private_result = commands.LoadPipettePrivateResult( + pipette_id="pipette-id", + serial_number="pipette-serial", + config=LoadedStaticPipetteData( + channels=1, + max_volume=15, + min_volume=3, + model="gen a", + display_name="display name", + flow_rates=FlowRates( + default_aspirate={}, + default_dispense={}, + default_blow_out={}, + ), + tip_configuration_lookup_table={15: supported_tip_fixture}, + nominal_tip_overlap={}, + nozzle_offset_z=1.23, + home_position=4.56, + nozzle_map=get_default_nozzle_map(PipetteNameType.P300_SINGLE_GEN2), + back_left_corner_offset=Point(x=1, y=2, z=3), + front_right_corner_offset=Point(x=4, y=5, z=6), + ), + ) + subject.handle_action( + actions.UpdateCommandAction( + private_result=load_pipette_private_result, command=load_pipette_command + ) + ) result = TipView(subject.state).get_next_tip( labware_id="cool-labware", num_tips=1, starting_tip_name="B2", + nozzle_map=load_pipette_private_result.config.nozzle_map, ) assert result == "B2" @@ -278,6 +438,7 @@ def test_get_next_tip_with_starting_tip( labware_id="cool-labware", num_tips=1, starting_tip_name="B2", + nozzle_map=load_pipette_private_result.config.nozzle_map, ) assert result == "C2" @@ -286,16 +447,49 @@ def test_get_next_tip_with_starting_tip( def test_get_next_tip_with_starting_tip_8_channel( subject: TipStore, load_labware_command: commands.LoadLabware, + supported_tip_fixture: pipette_definition.SupportedTipsDefinition, ) -> None: """It should return the starting tip, and then the following tip after that.""" subject.handle_action( actions.UpdateCommandAction(private_result=None, command=load_labware_command) ) + load_pipette_command = commands.LoadPipette.construct( # type: ignore[call-arg] + result=commands.LoadPipetteResult(pipetteId="pipette-id") + ) + load_pipette_private_result = commands.LoadPipettePrivateResult( + pipette_id="pipette-id", + serial_number="pipette-serial", + config=LoadedStaticPipetteData( + channels=8, + max_volume=15, + min_volume=3, + model="gen a", + display_name="display name", + flow_rates=FlowRates( + default_aspirate={}, + default_dispense={}, + default_blow_out={}, + ), + tip_configuration_lookup_table={15: supported_tip_fixture}, + nominal_tip_overlap={}, + nozzle_offset_z=1.23, + home_position=4.56, + nozzle_map=get_default_nozzle_map(PipetteNameType.P300_MULTI_GEN2), + back_left_corner_offset=Point(0, 0, 0), + front_right_corner_offset=Point(0, 0, 0), + ), + ) + subject.handle_action( + actions.UpdateCommandAction( + private_result=load_pipette_private_result, command=load_pipette_command + ) + ) result = TipView(subject.state).get_next_tip( labware_id="cool-labware", num_tips=8, starting_tip_name="A2", + nozzle_map=None, ) assert result == "A2" @@ -319,6 +513,7 @@ def test_get_next_tip_with_starting_tip_8_channel( labware_id="cool-labware", num_tips=8, starting_tip_name="A2", + nozzle_map=None, ) assert result == "A3" @@ -327,16 +522,49 @@ def test_get_next_tip_with_starting_tip_8_channel( def test_get_next_tip_with_starting_tip_out_of_tips( subject: TipStore, load_labware_command: commands.LoadLabware, + supported_tip_fixture: pipette_definition.SupportedTipsDefinition, ) -> None: """It should return the starting tip of H12 and then None after that.""" subject.handle_action( actions.UpdateCommandAction(private_result=None, command=load_labware_command) ) + load_pipette_command = commands.LoadPipette.construct( # type: ignore[call-arg] + result=commands.LoadPipetteResult(pipetteId="pipette-id") + ) + load_pipette_private_result = commands.LoadPipettePrivateResult( + pipette_id="pipette-id", + serial_number="pipette-serial", + config=LoadedStaticPipetteData( + channels=1, + max_volume=15, + min_volume=3, + model="gen a", + display_name="display name", + flow_rates=FlowRates( + default_aspirate={}, + default_dispense={}, + default_blow_out={}, + ), + tip_configuration_lookup_table={15: supported_tip_fixture}, + nominal_tip_overlap={}, + nozzle_offset_z=1.23, + home_position=4.56, + nozzle_map=get_default_nozzle_map(PipetteNameType.P300_SINGLE_GEN2), + back_left_corner_offset=Point(0, 0, 0), + front_right_corner_offset=Point(0, 0, 0), + ), + ) + subject.handle_action( + actions.UpdateCommandAction( + private_result=load_pipette_private_result, command=load_pipette_command + ) + ) result = TipView(subject.state).get_next_tip( labware_id="cool-labware", num_tips=1, starting_tip_name="H12", + nozzle_map=None, ) assert result == "H12" @@ -360,6 +588,7 @@ def test_get_next_tip_with_starting_tip_out_of_tips( labware_id="cool-labware", num_tips=1, starting_tip_name="H12", + nozzle_map=None, ) assert result is None @@ -368,16 +597,49 @@ def test_get_next_tip_with_starting_tip_out_of_tips( def test_get_next_tip_with_column_and_starting_tip( subject: TipStore, load_labware_command: commands.LoadLabware, + supported_tip_fixture: pipette_definition.SupportedTipsDefinition, ) -> None: """It should return the first tip in a column, taking starting tip into account.""" subject.handle_action( actions.UpdateCommandAction(private_result=None, command=load_labware_command) ) + load_pipette_command = commands.LoadPipette.construct( # type: ignore[call-arg] + result=commands.LoadPipetteResult(pipetteId="pipette-id") + ) + load_pipette_private_result = commands.LoadPipettePrivateResult( + pipette_id="pipette-id", + serial_number="pipette-serial", + config=LoadedStaticPipetteData( + channels=8, + max_volume=15, + min_volume=3, + model="gen a", + display_name="display name", + flow_rates=FlowRates( + default_aspirate={}, + default_dispense={}, + default_blow_out={}, + ), + tip_configuration_lookup_table={15: supported_tip_fixture}, + nominal_tip_overlap={}, + nozzle_offset_z=1.23, + home_position=4.56, + nozzle_map=get_default_nozzle_map(PipetteNameType.P300_MULTI_GEN2), + back_left_corner_offset=Point(0, 0, 0), + front_right_corner_offset=Point(0, 0, 0), + ), + ) + subject.handle_action( + actions.UpdateCommandAction( + private_result=load_pipette_private_result, command=load_pipette_command + ) + ) result = TipView(subject.state).get_next_tip( labware_id="cool-labware", num_tips=8, starting_tip_name="D1", + nozzle_map=None, ) assert result == "A2" @@ -400,7 +662,7 @@ def test_reset_tips( pipette_id="pipette-id", serial_number="pipette-serial", config=LoadedStaticPipetteData( - channels=8, + channels=1, max_volume=15, min_volume=3, model="gen a", @@ -435,6 +697,7 @@ def test_reset_tips( labware_id="cool-labware", num_tips=1, starting_tip_name=None, + nozzle_map=None, ) assert result == "A1" @@ -759,5 +1022,117 @@ def test_next_tip_uses_active_channels( labware_id="cool-labware", num_tips=5, starting_tip_name=None, + nozzle_map=None, ) assert result == "A2" + + +def test_next_tip_automatic_tip_tracking_with_partial_configurations( + subject: TipStore, + supported_tip_fixture: pipette_definition.SupportedTipsDefinition, + load_labware_command: commands.LoadLabware, + pick_up_tip_command: commands.PickUpTip, +) -> None: + """Test tip tracking logic using multiple pipette configurations.""" + # Load labware + subject.handle_action( + actions.UpdateCommandAction(private_result=None, command=load_labware_command) + ) + + # Load pipette + load_pipette_command = commands.LoadPipette.construct( # type: ignore[call-arg] + result=commands.LoadPipetteResult(pipetteId="pipette-id") + ) + load_pipette_private_result = commands.LoadPipettePrivateResult( + pipette_id="pipette-id", + serial_number="pipette-serial", + config=LoadedStaticPipetteData( + channels=96, + max_volume=15, + min_volume=3, + model="gen a", + display_name="display name", + flow_rates=FlowRates( + default_aspirate={}, + default_dispense={}, + default_blow_out={}, + ), + tip_configuration_lookup_table={15: supported_tip_fixture}, + nominal_tip_overlap={}, + nozzle_offset_z=1.23, + home_position=4.56, + nozzle_map=get_default_nozzle_map(PipetteNameType.P1000_96), + back_left_corner_offset=Point(x=1, y=2, z=3), + front_right_corner_offset=Point(x=4, y=5, z=6), + ), + ) + subject.handle_action( + actions.UpdateCommandAction( + private_result=load_pipette_private_result, command=load_pipette_command + ) + ) + + def _assert_and_pickup(well: str, nozzle_map: NozzleMap) -> None: + result = TipView(subject.state).get_next_tip( + labware_id="cool-labware", + num_tips=0, + starting_tip_name=None, + nozzle_map=nozzle_map, + ) + assert result == well + + pick_up_tip = commands.PickUpTip.construct( # type: ignore[call-arg] + params=commands.PickUpTipParams.construct( + pipetteId="pipette-id", + labwareId="cool-labware", + wellName=result, + ), + result=commands.PickUpTipResult.construct( + position=DeckPoint(x=0, y=0, z=0), tipLength=1.23 + ), + ) + + subject.handle_action( + actions.UpdateCommandAction(private_result=None, command=pick_up_tip) + ) + + # Configure nozzle for partial configurations + configure_nozzle_layout_cmd = commands.ConfigureNozzleLayout.construct( # type: ignore[call-arg] + result=commands.ConfigureNozzleLayoutResult() + ) + + def _reconfigure_nozzle_layout(start: str, back_l: str, front_r: str) -> NozzleMap: + + configure_nozzle_private_result = commands.ConfigureNozzleLayoutPrivateResult( + pipette_id="pipette-id", + nozzle_map=NozzleMap.build( + physical_nozzles=NINETY_SIX_MAP, + physical_rows=NINETY_SIX_ROWS, + physical_columns=NINETY_SIX_COLS, + starting_nozzle=start, + back_left_nozzle=back_l, + front_right_nozzle=front_r, + ), + ) + subject.handle_action( + actions.UpdateCommandAction( + private_result=configure_nozzle_private_result, + command=configure_nozzle_layout_cmd, + ) + ) + return configure_nozzle_private_result.nozzle_map + + map = _reconfigure_nozzle_layout("A1", "A1", "H10") + _assert_and_pickup("A3", map) + map = _reconfigure_nozzle_layout("A1", "A1", "F2") + _assert_and_pickup("C1", map) + + # Configure to single tip pickups + map = _reconfigure_nozzle_layout("H12", "H12", "H12") + _assert_and_pickup("A1", map) + map = _reconfigure_nozzle_layout("H1", "H1", "H1") + _assert_and_pickup("A2", map) + map = _reconfigure_nozzle_layout("A12", "A12", "A12") + _assert_and_pickup("B1", map) + map = _reconfigure_nozzle_layout("A1", "A1", "A1") + _assert_and_pickup("B2", map) diff --git a/app-shell-odd/src/notify.ts b/app-shell-odd/src/notify.ts index be9af060346..f88280369a0 100644 --- a/app-shell-odd/src/notify.ts +++ b/app-shell-odd/src/notify.ts @@ -1,13 +1,19 @@ /* eslint-disable @typescript-eslint/no-dynamic-delete */ import mqtt from 'mqtt' +import isEqual from 'lodash/isEqual' import { createLogger } from './log' import type { BrowserWindow } from 'electron' -import type { NotifyTopic } from '@opentrons/app/src/redux/shell/types' +import type { + NotifyTopic, + NotifyResponseData, + NotifyRefetchData, + NotifyUnsubscribeData, + NotifyNetworkError, +} from '@opentrons/app/src/redux/shell/types' import type { Action, Dispatch } from './types' -// TODO(jh, 2024-01-22): refactor the ODD connection store to manage a single client only. // TODO(jh, 2024-03-01): after refactoring notify connectivity and subscription logic, uncomment logs. // Manages MQTT broker connections via a connection store, establishing a connection to the broker only if a connection does not @@ -123,7 +129,7 @@ function subscribe(notifyParams: NotifyParams): Promise { log.warn( `Failed to connect to ${hostname} - ${error.name}: ${error.message} ` ) - let failureMessage: string = FAILURE_STATUSES.ECONNFAILED + let failureMessage: NotifyNetworkError = FAILURE_STATUSES.ECONNFAILED if (connectionStore[hostname]?.client == null) { unreachableHosts.add(hostname) if ( @@ -184,7 +190,6 @@ function subscribe(notifyParams: NotifyParams): Promise { function subscribeCb(error: Error, result: mqtt.ISubscriptionGrant[]): void { const { subscriptions } = connectionStore[hostname] if (error != null) { - // log.warn(`Failed to subscribe on ${hostname} to topic: ${topic}`) sendToBrowserDeserialized({ browserWindow, hostname, @@ -197,7 +202,6 @@ function subscribe(notifyParams: NotifyParams): Promise { } }, RENDER_TIMEOUT) } else { - // log.info(`Successfully subscribed on ${hostname} to topic: ${topic}`) if (subscriptions[topic] > 0) { subscriptions[topic] += 1 } else { @@ -227,7 +231,6 @@ function subscribe(notifyParams: NotifyParams): Promise { counter++ if (counter === MAX_RETRIES) { clearInterval(intervalId) - // log.warn(`Failed to subscribe on ${hostname} to topic: ${topic}`) reject(new Error('Maximum subscription retries exceeded.')) } }, CHECK_CONNECTION_INTERVAL) @@ -252,24 +255,15 @@ function unsubscribe(notifyParams: NotifyParams): Promise { if (isLastSubscription) { client?.unsubscribe(topic, {}, (error, result) => { - if (error != null) { - // log.warn( - // `Failed to unsubscribe on ${hostname} from topic: ${topic}` - // ) - } else { - // log.info( - // `Successfully unsubscribed on ${hostname} from topic: ${topic}` - // ) + if (error == null) { handleDecrementSubscriptionCount(hostname, topic) + } else { + log.warn(`Failed to subscribe on ${hostname} to topic: ${topic}`) } }) } else { subscriptions[topic] -= 1 } - } else { - // log.info( - // `Attempted to unsubscribe from unconnected hostname: ${hostname}` - // ) } }, RENDER_TIMEOUT) }) @@ -344,12 +338,21 @@ function establishListeners({ client.on( 'message', (topic: NotifyTopic, message: Buffer, packet: mqtt.IPublishPacket) => { - sendToBrowserDeserialized({ - browserWindow, - hostname, - topic, - message: message.toString(), - }) + deserialize(message.toString()) + .then(deserializedMessage => { + log.debug('Received notification data from main via IPC', { + hostname, + topic, + }) + + browserWindow.webContents.send( + 'notify', + hostname, + topic, + deserializedMessage + ) + }) + .catch(error => log.debug(`${error.message}`)) } ) @@ -410,7 +413,7 @@ interface SendToBrowserParams { browserWindow: BrowserWindow hostname: string topic: NotifyTopic - message: string + message: NotifyResponseData } function sendToBrowserDeserialized({ @@ -419,18 +422,34 @@ function sendToBrowserDeserialized({ topic, message, }: SendToBrowserParams): void { - let deserializedMessage: string | Object + browserWindow.webContents.send('notify', hostname, topic, message) +} - try { - deserializedMessage = JSON.parse(message) - } catch { - deserializedMessage = message - } +const VALID_MODELS: [NotifyRefetchData, NotifyUnsubscribeData] = [ + { refetchUsingHTTP: true }, + { unsubscribe: true }, +] + +function deserialize(message: string): Promise { + return new Promise((resolve, reject) => { + let deserializedMessage: NotifyResponseData | Record + const error = new Error( + `Unexpected data received from notify broker: ${message}` + ) - // log.info('Received notification data from main via IPC', { - // hostname, - // topic, - // }) + try { + deserializedMessage = JSON.parse(message) + } catch { + reject(error) + } - browserWindow.webContents.send('notify', hostname, topic, deserializedMessage) + const isValidNotifyResponse = VALID_MODELS.some(model => + isEqual(model, deserializedMessage) + ) + if (!isValidNotifyResponse) { + reject(error) + } else { + resolve(JSON.parse(message)) + } + }) } diff --git a/app-shell/src/notify.ts b/app-shell/src/notify.ts index 95dfcbdcac3..3de2281a385 100644 --- a/app-shell/src/notify.ts +++ b/app-shell/src/notify.ts @@ -1,10 +1,17 @@ /* eslint-disable @typescript-eslint/no-dynamic-delete */ import mqtt from 'mqtt' +import isEqual from 'lodash/isEqual' import { createLogger } from './log' import type { BrowserWindow } from 'electron' -import type { NotifyTopic } from '@opentrons/app/src/redux/shell/types' +import type { + NotifyTopic, + NotifyResponseData, + NotifyRefetchData, + NotifyUnsubscribeData, + NotifyNetworkError, +} from '@opentrons/app/src/redux/shell/types' import type { Action, Dispatch } from './types' // TODO(jh, 2024-03-01): after refactoring notify connectivity and subscription logic, uncomment logs. @@ -120,7 +127,7 @@ function subscribe(notifyParams: NotifyParams): Promise { log.warn( `Failed to connect to ${hostname} - ${error.name}: ${error.message} ` ) - let failureMessage: string = FAILURE_STATUSES.ECONNFAILED + let failureMessage: NotifyNetworkError = FAILURE_STATUSES.ECONNFAILED if (connectionStore[hostname]?.client == null) { unreachableHosts.add(hostname) if ( @@ -181,7 +188,6 @@ function subscribe(notifyParams: NotifyParams): Promise { function subscribeCb(error: Error, result: mqtt.ISubscriptionGrant[]): void { const { subscriptions } = connectionStore[hostname] if (error != null) { - // log.warn(`Failed to subscribe on ${hostname} to topic: ${topic}`) sendToBrowserDeserialized({ browserWindow, hostname, @@ -194,7 +200,6 @@ function subscribe(notifyParams: NotifyParams): Promise { } }, RENDER_TIMEOUT) } else { - // log.info(`Successfully subscribed on ${hostname} to topic: ${topic}`) if (subscriptions[topic] > 0) { subscriptions[topic] += 1 } else { @@ -224,7 +229,6 @@ function subscribe(notifyParams: NotifyParams): Promise { counter++ if (counter === MAX_RETRIES) { clearInterval(intervalId) - // log.warn(`Failed to subscribe on ${hostname} to topic: ${topic}`) reject(new Error('Maximum subscription retries exceeded.')) } }, CHECK_CONNECTION_INTERVAL) @@ -249,24 +253,15 @@ function unsubscribe(notifyParams: NotifyParams): Promise { if (isLastSubscription) { client?.unsubscribe(topic, {}, (error, result) => { - if (error != null) { - // log.warn( - // `Failed to unsubscribe on ${hostname} from topic: ${topic}` - // ) - } else { - // log.info( - // `Successfully unsubscribed on ${hostname} from topic: ${topic}` - // ) + if (error == null) { handleDecrementSubscriptionCount(hostname, topic) + } else { + log.warn(`Failed to subscribe on ${hostname} to topic: ${topic}`) } }) } else { subscriptions[topic] -= 1 } - } else { - // log.info( - // `Attempted to unsubscribe from unconnected hostname: ${hostname}` - // ) } }, RENDER_TIMEOUT) }) @@ -341,12 +336,21 @@ function establishListeners({ client.on( 'message', (topic: NotifyTopic, message: Buffer, packet: mqtt.IPublishPacket) => { - sendToBrowserDeserialized({ - browserWindow, - hostname, - topic, - message: message.toString(), - }) + deserialize(message.toString()) + .then(deserializedMessage => { + log.debug('Received notification data from main via IPC', { + hostname, + topic, + }) + + browserWindow.webContents.send( + 'notify', + hostname, + topic, + deserializedMessage + ) + }) + .catch(error => log.debug(`${error.message}`)) } ) @@ -407,7 +411,7 @@ interface SendToBrowserParams { browserWindow: BrowserWindow hostname: string topic: NotifyTopic - message: string + message: NotifyResponseData } function sendToBrowserDeserialized({ @@ -416,18 +420,34 @@ function sendToBrowserDeserialized({ topic, message, }: SendToBrowserParams): void { - let deserializedMessage: string | Object + browserWindow.webContents.send('notify', hostname, topic, message) +} - try { - deserializedMessage = JSON.parse(message) - } catch { - deserializedMessage = message - } +const VALID_MODELS: [NotifyRefetchData, NotifyUnsubscribeData] = [ + { refetchUsingHTTP: true }, + { unsubscribe: true }, +] + +function deserialize(message: string): Promise { + return new Promise((resolve, reject) => { + let deserializedMessage: NotifyResponseData | Record + const error = new Error( + `Unexpected data received from notify broker: ${message}` + ) - // log.info('Received notification data from main via IPC', { - // hostname, - // topic, - // }) + try { + deserializedMessage = JSON.parse(message) + } catch { + reject(error) + } - browserWindow.webContents.send('notify', hostname, topic, deserializedMessage) + const isValidNotifyResponse = VALID_MODELS.some(model => + isEqual(model, deserializedMessage) + ) + if (!isValidNotifyResponse) { + reject(error) + } else { + resolve(JSON.parse(message)) + } + }) } diff --git a/app/src/App/__tests__/OnDeviceDisplayApp.test.tsx b/app/src/App/__tests__/OnDeviceDisplayApp.test.tsx index d1a7307b77c..ba9b852923e 100644 --- a/app/src/App/__tests__/OnDeviceDisplayApp.test.tsx +++ b/app/src/App/__tests__/OnDeviceDisplayApp.test.tsx @@ -27,7 +27,7 @@ import { getIsShellReady } from '../../redux/shell' import { getLocalRobot } from '../../redux/discovery' import { mockConnectedRobot } from '../../redux/discovery/__fixtures__' import { useCurrentRunRoute, useProtocolReceiptToast } from '../hooks' -import { useNotifyCurrentMaintenanceRun } from '../../resources/maintenance_runs/useNotifyCurrentMaintenanceRun' +import { useNotifyCurrentMaintenanceRun } from '../../resources/maintenance_runs' import type { OnDeviceDisplaySettings } from '../../redux/config/schema-types' @@ -51,7 +51,7 @@ vi.mock('../../pages/DeckConfiguration') vi.mock('../../redux/config') vi.mock('../../redux/shell') vi.mock('../../redux/discovery') -vi.mock('../../resources/maintenance_runs/useNotifyCurrentMaintenanceRun') +vi.mock('../../resources/maintenance_runs') vi.mock('../hooks') const mockSettings = { diff --git a/app/src/App/hooks.ts b/app/src/App/hooks.ts index cbc40b396eb..a7db8ed203f 100644 --- a/app/src/App/hooks.ts +++ b/app/src/App/hooks.ts @@ -22,8 +22,7 @@ import { import { checkShellUpdate } from '../redux/shell' import { useToaster } from '../organisms/ToasterOven' -import { useNotifyAllRunsQuery } from '../resources/runs/useNotifyAllRunsQuery' -import { useNotifyRunQuery } from '../resources/runs/useNotifyRunQuery' +import { useNotifyAllRunsQuery, useNotifyRunQuery } from '../resources/runs' import type { SetStatusBarCreateCommand } from '@opentrons/shared-data' import type { Dispatch } from '../redux/types' diff --git a/app/src/assets/localization/en/app_settings.json b/app/src/assets/localization/en/app_settings.json index b8d7bdc4c2c..017cf97914c 100644 --- a/app/src/assets/localization/en/app_settings.json +++ b/app/src/assets/localization/en/app_settings.json @@ -1,5 +1,6 @@ { "__dev_internal__protocolStats": "Protocol Stats", + "__dev_internal__enableRunTimeParameters": "Enable Run Time Parameters", "add_folder_button": "Add labware source folder", "add_ip_button": "Add", "add_ip_error": "Enter an IP Address or Hostname", diff --git a/app/src/atoms/InlineNotification/InlineNotification.stories.tsx b/app/src/atoms/InlineNotification/InlineNotification.stories.tsx index 71fca5a8277..313d278c0fa 100644 --- a/app/src/atoms/InlineNotification/InlineNotification.stories.tsx +++ b/app/src/atoms/InlineNotification/InlineNotification.stories.tsx @@ -13,9 +13,9 @@ export default { defaultValue: false, }, type: { + options: ['alert', 'error', 'neutral', 'success'], control: { type: 'select', - options: ['alert', 'error', 'neutral', 'success'], }, defaultValue: 'success', }, diff --git a/app/src/atoms/InlineNotification/index.tsx b/app/src/atoms/InlineNotification/index.tsx index a92492d7c46..03294967bae 100644 --- a/app/src/atoms/InlineNotification/index.tsx +++ b/app/src/atoms/InlineNotification/index.tsx @@ -46,8 +46,8 @@ const INLINE_NOTIFICATION_PROPS_BY_TYPE: Record< }, neutral: { icon: { name: 'information' }, - backgroundColor: COLORS.grey30, - color: COLORS.grey60, + backgroundColor: COLORS.blue30, + color: COLORS.blue60, }, success: { icon: { name: 'ot-check' }, diff --git a/app/src/organisms/ApplyHistoricOffsets/hooks/__tests__/useOffsetCandidatesForAnalysis.test.tsx b/app/src/organisms/ApplyHistoricOffsets/hooks/__tests__/useOffsetCandidatesForAnalysis.test.tsx index b442cef4b41..75c34f1f843 100644 --- a/app/src/organisms/ApplyHistoricOffsets/hooks/__tests__/useOffsetCandidatesForAnalysis.test.tsx +++ b/app/src/organisms/ApplyHistoricOffsets/hooks/__tests__/useOffsetCandidatesForAnalysis.test.tsx @@ -19,6 +19,8 @@ import type { OffsetCandidate } from '../useOffsetCandidatesForAnalysis' vi.mock('../useAllHistoricOffsets') vi.mock('../getLabwareLocationCombos') vi.mock('@opentrons/shared-data') +vi.mock('../../../../resources/runs') +vi.mock('../../../../resources/useNotifyService') const mockLabwareDef = fixtureTiprack300ul as LabwareDefinition2 diff --git a/app/src/organisms/ChooseRobotSlideout/AvailableRobotOption.tsx b/app/src/organisms/ChooseRobotSlideout/AvailableRobotOption.tsx index 721a7fbad49..cb277f6fb2a 100644 --- a/app/src/organisms/ChooseRobotSlideout/AvailableRobotOption.tsx +++ b/app/src/organisms/ChooseRobotSlideout/AvailableRobotOption.tsx @@ -23,7 +23,7 @@ import { appShellRequestor } from '../../redux/shell/remote' import OT2_PNG from '../../assets/images/OT2-R_HERO.png' import FLEX_PNG from '../../assets/images/FLEX.png' import { RobotBusyStatusAction } from '.' -import { useNotifyAllRunsQuery } from '../../resources/runs/useNotifyAllRunsQuery' +import { useNotifyAllRunsQuery } from '../../resources/runs' import type { IconName } from '@opentrons/components' import type { Runs } from '@opentrons/api-client' diff --git a/app/src/organisms/DeviceDetailsDeckConfiguration/__tests__/DeviceDetailsDeckConfiguration.test.tsx b/app/src/organisms/DeviceDetailsDeckConfiguration/__tests__/DeviceDetailsDeckConfiguration.test.tsx index 00464783c23..f3b008320af 100644 --- a/app/src/organisms/DeviceDetailsDeckConfiguration/__tests__/DeviceDetailsDeckConfiguration.test.tsx +++ b/app/src/organisms/DeviceDetailsDeckConfiguration/__tests__/DeviceDetailsDeckConfiguration.test.tsx @@ -15,7 +15,7 @@ import { useIsRobotViewable, useRunStatuses } from '../../Devices/hooks' import { DeckFixtureSetupInstructionsModal } from '../DeckFixtureSetupInstructionsModal' import { useIsEstopNotDisengaged } from '../../../resources/devices/hooks/useIsEstopNotDisengaged' import { DeviceDetailsDeckConfiguration } from '../' -import { useNotifyCurrentMaintenanceRun } from '../../../resources/maintenance_runs/useNotifyCurrentMaintenanceRun' +import { useNotifyCurrentMaintenanceRun } from '../../../resources/maintenance_runs' import type { MaintenanceRun } from '@opentrons/api-client' import type * as OpentronsComponents from '@opentrons/components' @@ -30,7 +30,7 @@ vi.mock('@opentrons/components', async importOriginal => { vi.mock('@opentrons/react-api-client') vi.mock('../DeckFixtureSetupInstructionsModal') vi.mock('../../Devices/hooks') -vi.mock('../../../resources/maintenance_runs/useNotifyCurrentMaintenanceRun') +vi.mock('../../../resources/maintenance_runs') vi.mock('../../../resources/devices/hooks/useIsEstopNotDisengaged') const ROBOT_NAME = 'otie' diff --git a/app/src/organisms/DeviceDetailsDeckConfiguration/index.tsx b/app/src/organisms/DeviceDetailsDeckConfiguration/index.tsx index 4e51bd06fb0..a3310c0fae5 100644 --- a/app/src/organisms/DeviceDetailsDeckConfiguration/index.tsx +++ b/app/src/organisms/DeviceDetailsDeckConfiguration/index.tsx @@ -31,7 +31,7 @@ import { SINGLE_RIGHT_SLOT_FIXTURE, } from '@opentrons/shared-data' -import { useNotifyCurrentMaintenanceRun } from '../../resources/maintenance_runs/useNotifyCurrentMaintenanceRun' +import { useNotifyCurrentMaintenanceRun } from '../../resources/maintenance_runs' import { StyledText } from '../../atoms/text' import { Banner } from '../../atoms/Banner' import { DeckFixtureSetupInstructionsModal } from './DeckFixtureSetupInstructionsModal' diff --git a/app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader.tsx b/app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader.tsx index e80bf386580..8d7737a3007 100644 --- a/app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader.tsx +++ b/app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader.tsx @@ -107,7 +107,7 @@ import { getIsFixtureMismatch } from '../../../resources/deck_configuration/util import { useDeckConfigurationCompatibility } from '../../../resources/deck_configuration/hooks' import { useMostRecentCompletedAnalysis } from '../../LabwarePositionCheck/useMostRecentCompletedAnalysis' import { useMostRecentRunId } from '../../ProtocolUpload/hooks/useMostRecentRunId' -import { useNotifyRunQuery } from '../../../resources/runs/useNotifyRunQuery' +import { useNotifyRunQuery } from '../../../resources/runs' import type { Run, RunError } from '@opentrons/api-client' import type { State } from '../../../redux/types' diff --git a/app/src/organisms/Devices/ProtocolRun/SetupLabware/__tests__/SetupLabware.test.tsx b/app/src/organisms/Devices/ProtocolRun/SetupLabware/__tests__/SetupLabware.test.tsx index 46e41492da2..0e19191306d 100644 --- a/app/src/organisms/Devices/ProtocolRun/SetupLabware/__tests__/SetupLabware.test.tsx +++ b/app/src/organisms/Devices/ProtocolRun/SetupLabware/__tests__/SetupLabware.test.tsx @@ -19,7 +19,7 @@ import { import { SetupLabwareList } from '../SetupLabwareList' import { SetupLabwareMap } from '../SetupLabwareMap' import { SetupLabware } from '..' -import { useNotifyRunQuery } from '../../../../../resources/runs/useNotifyRunQuery' +import { useNotifyRunQuery } from '../../../../../resources/runs' vi.mock('../SetupLabwareList') vi.mock('../SetupLabwareMap') @@ -29,7 +29,7 @@ vi.mock('../../../../RunTimeControl/hooks') vi.mock('../../../../../redux/config') vi.mock('../../../hooks') vi.mock('../../../hooks/useLPCSuccessToast') -vi.mock('../../../../../resources/runs/useNotifyRunQuery') +vi.mock('../../../../../resources/runs') const ROBOT_NAME = 'otie' const RUN_ID = '1' diff --git a/app/src/organisms/Devices/ProtocolRun/SetupLabwarePositionCheck/__tests__/SetupLabwarePositionCheck.test.tsx b/app/src/organisms/Devices/ProtocolRun/SetupLabwarePositionCheck/__tests__/SetupLabwarePositionCheck.test.tsx index abae4830e68..98bfe60da4a 100644 --- a/app/src/organisms/Devices/ProtocolRun/SetupLabwarePositionCheck/__tests__/SetupLabwarePositionCheck.test.tsx +++ b/app/src/organisms/Devices/ProtocolRun/SetupLabwarePositionCheck/__tests__/SetupLabwarePositionCheck.test.tsx @@ -24,7 +24,7 @@ import { useRobotType, } from '../../../hooks' import { SetupLabwarePositionCheck } from '..' -import { useNotifyRunQuery } from '../../../../../resources/runs/useNotifyRunQuery' +import { useNotifyRunQuery } from '../../../../../resources/runs' import type { Mock } from 'vitest' @@ -35,7 +35,7 @@ vi.mock('../../../../../redux/config') vi.mock('../../../hooks') vi.mock('../../../hooks/useLPCSuccessToast') vi.mock('@opentrons/react-api-client') -vi.mock('../../../../../resources/runs/useNotifyRunQuery') +vi.mock('../../../../../resources/runs') const DISABLED_REASON = 'MOCK_DISABLED_REASON' const ROBOT_NAME = 'otie' diff --git a/app/src/organisms/Devices/ProtocolRun/SetupLabwarePositionCheck/index.tsx b/app/src/organisms/Devices/ProtocolRun/SetupLabwarePositionCheck/index.tsx index 383aa273588..97575ad2cf2 100644 --- a/app/src/organisms/Devices/ProtocolRun/SetupLabwarePositionCheck/index.tsx +++ b/app/src/organisms/Devices/ProtocolRun/SetupLabwarePositionCheck/index.tsx @@ -26,7 +26,7 @@ import { CurrentOffsetsTable } from './CurrentOffsetsTable' import { useLaunchLPC } from '../../../LabwarePositionCheck/useLaunchLPC' import { StyledText } from '../../../../atoms/text' import { getLatestCurrentOffsets } from './utils' -import { useNotifyRunQuery } from '../../../../resources/runs/useNotifyRunQuery' +import { useNotifyRunQuery } from '../../../../resources/runs' import type { LabwareOffset } from '@opentrons/api-client' diff --git a/app/src/organisms/Devices/ProtocolRun/SetupLiquids/__tests__/SetupLiquidsList.test.tsx b/app/src/organisms/Devices/ProtocolRun/SetupLiquids/__tests__/SetupLiquidsList.test.tsx index a8d659b5cc4..4dbfd57cf78 100644 --- a/app/src/organisms/Devices/ProtocolRun/SetupLiquids/__tests__/SetupLiquidsList.test.tsx +++ b/app/src/organisms/Devices/ProtocolRun/SetupLiquids/__tests__/SetupLiquidsList.test.tsx @@ -25,7 +25,7 @@ import { getTotalVolumePerLiquidLabwarePair, } from '../utils' import { LiquidsLabwareDetailsModal } from '../LiquidsLabwareDetailsModal' -import { useNotifyRunQuery } from '../../../../../resources/runs/useNotifyRunQuery' +import { useNotifyRunQuery } from '../../../../../resources/runs' import type { Mock } from 'vitest' @@ -61,7 +61,7 @@ vi.mock('../../utils/getLocationInfoNames') vi.mock('../LiquidsLabwareDetailsModal') vi.mock('@opentrons/api-client') vi.mock('../../../../../redux/analytics') -vi.mock('../../../../../resources/runs/useNotifyRunQuery') +vi.mock('../../../../../resources/runs') const render = (props: React.ComponentProps) => { return renderWithProviders(, { diff --git a/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/SetupModulesList.tsx b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/SetupModulesList.tsx index ca4413bb5e9..641a22680a8 100644 --- a/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/SetupModulesList.tsx +++ b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/SetupModulesList.tsx @@ -35,7 +35,7 @@ import { TertiaryButton } from '../../../../atoms/buttons' import { StatusLabel } from '../../../../atoms/StatusLabel' import { StyledText } from '../../../../atoms/text' import { Tooltip } from '../../../../atoms/Tooltip' -import { useChainLiveCommands } from '../../../../resources/runs/hooks' +import { useChainLiveCommands } from '../../../../resources/runs' import { ModuleSetupModal } from '../../../ModuleCard/ModuleSetupModal' import { ModuleWizardFlows } from '../../../ModuleWizardFlows' import { getModulePrepCommands } from '../../getModulePrepCommands' diff --git a/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/SetupModulesList.test.tsx b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/SetupModulesList.test.tsx index c772a7acbab..05df2fc9cef 100644 --- a/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/SetupModulesList.test.tsx +++ b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/SetupModulesList.test.tsx @@ -14,7 +14,7 @@ import { mockMagneticModuleGen2, mockThermocycler, } from '../../../../../redux/modules/__fixtures__' -import { useChainLiveCommands } from '../../../../../resources/runs/hooks' +import { useChainLiveCommands } from '../../../../../resources/runs' import { ModuleSetupModal } from '../../../../ModuleCard/ModuleSetupModal' import { ModuleWizardFlows } from '../../../../ModuleWizardFlows' import { @@ -38,7 +38,7 @@ vi.mock('../UnMatchedModuleWarning') vi.mock('../../../../ModuleCard/ModuleSetupModal') vi.mock('../../../../ModuleWizardFlows') vi.mock('../MultipleModulesModal') -vi.mock('../../../../../resources/runs/hooks') +vi.mock('../../../../../resources/runs') vi.mock('../../../../../redux/config') const ROBOT_NAME = 'otie' diff --git a/app/src/organisms/Devices/ProtocolRun/__tests__/ProtocolRunHeader.test.tsx b/app/src/organisms/Devices/ProtocolRun/__tests__/ProtocolRunHeader.test.tsx index b2359f77dba..65ea98c906f 100644 --- a/app/src/organisms/Devices/ProtocolRun/__tests__/ProtocolRunHeader.test.tsx +++ b/app/src/organisms/Devices/ProtocolRun/__tests__/ProtocolRunHeader.test.tsx @@ -95,7 +95,7 @@ import { getIsFixtureMismatch } from '../../../../resources/deck_configuration/u import { useDeckConfigurationCompatibility } from '../../../../resources/deck_configuration/hooks' import { useMostRecentCompletedAnalysis } from '../../../LabwarePositionCheck/useMostRecentCompletedAnalysis' import { useMostRecentRunId } from '../../../ProtocolUpload/hooks/useMostRecentRunId' -import { useNotifyRunQuery } from '../../../../resources/runs/useNotifyRunQuery' +import { useNotifyRunQuery } from '../../../../resources/runs' import type { UseQueryResult } from 'react-query' import type * as ReactRouterDom from 'react-router-dom' import type { Mock } from 'vitest' @@ -148,7 +148,7 @@ vi.mock('../../../../resources/deck_configuration/utils') vi.mock('../../../../resources/deck_configuration/hooks') vi.mock('../../../LabwarePositionCheck/useMostRecentCompletedAnalysis') vi.mock('../../../ProtocolUpload/hooks/useMostRecentRunId') -vi.mock('../../../../resources/runs/useNotifyRunQuery') +vi.mock('../../../../resources/runs') const ROBOT_NAME = 'otie' const RUN_ID = '95e67900-bc9f-4fbf-92c6-cc4d7226a51b' diff --git a/app/src/organisms/Devices/ProtocolRun/__tests__/ProtocolRunSetup.test.tsx b/app/src/organisms/Devices/ProtocolRun/__tests__/ProtocolRunSetup.test.tsx index 15f2dd374c5..92dc247b922 100644 --- a/app/src/organisms/Devices/ProtocolRun/__tests__/ProtocolRunSetup.test.tsx +++ b/app/src/organisms/Devices/ProtocolRun/__tests__/ProtocolRunSetup.test.tsx @@ -40,7 +40,7 @@ import { SetupLiquids } from '../SetupLiquids' import { SetupModuleAndDeck } from '../SetupModuleAndDeck' import { EmptySetupStep } from '../EmptySetupStep' import { ProtocolRunSetup } from '../ProtocolRunSetup' -import { useNotifyRunQuery } from '../../../../resources/runs/useNotifyRunQuery' +import { useNotifyRunQuery } from '../../../../resources/runs' import type * as SharedData from '@opentrons/shared-data' diff --git a/app/src/organisms/Devices/ProtocolRun/__tests__/SetupPipetteCalibration.test.tsx b/app/src/organisms/Devices/ProtocolRun/__tests__/SetupPipetteCalibration.test.tsx index bea43391bb9..12ee29a86cd 100644 --- a/app/src/organisms/Devices/ProtocolRun/__tests__/SetupPipetteCalibration.test.tsx +++ b/app/src/organisms/Devices/ProtocolRun/__tests__/SetupPipetteCalibration.test.tsx @@ -9,13 +9,13 @@ import { mockTipRackDefinition } from '../../../../redux/custom-labware/__fixtur import { useRunPipetteInfoByMount } from '../../hooks' import { SetupPipetteCalibrationItem } from '../SetupPipetteCalibrationItem' import { SetupInstrumentCalibration } from '../SetupInstrumentCalibration' -import { useNotifyRunQuery } from '../../../../resources/runs/useNotifyRunQuery' +import { useNotifyRunQuery } from '../../../../resources/runs' import type { PipetteInfo } from '../../hooks' vi.mock('../../hooks') vi.mock('../SetupPipetteCalibrationItem') -vi.mock('../../../../resources/runs/useNotifyRunQuery') +vi.mock('../../../../resources/runs') const ROBOT_NAME = 'otie' const RUN_ID = '1' diff --git a/app/src/organisms/Devices/ProtocolRun/useLabwareOffsetForLabware.ts b/app/src/organisms/Devices/ProtocolRun/useLabwareOffsetForLabware.ts index 07d4c838b85..f352ee2e40d 100644 --- a/app/src/organisms/Devices/ProtocolRun/useLabwareOffsetForLabware.ts +++ b/app/src/organisms/Devices/ProtocolRun/useLabwareOffsetForLabware.ts @@ -1,9 +1,9 @@ import { getLoadedLabwareDefinitionsByUri } from '@opentrons/shared-data' -import { getCurrentOffsetForLabwareInLocation } from '../../Devices/ProtocolRun/utils/getCurrentOffsetForLabwareInLocation' -import { getLabwareDefinitionUri } from '../../Devices/ProtocolRun/utils/getLabwareDefinitionUri' -import { getLabwareOffsetLocation } from '../../Devices/ProtocolRun/utils/getLabwareOffsetLocation' -import { useNotifyRunQuery } from '../../../resources/runs/useNotifyRunQuery' +import { getCurrentOffsetForLabwareInLocation } from './utils/getCurrentOffsetForLabwareInLocation' +import { getLabwareDefinitionUri } from './utils/getLabwareDefinitionUri' +import { getLabwareOffsetLocation } from './utils/getLabwareOffsetLocation' +import { useNotifyRunQuery } from '../../../resources/runs' import type { LabwareOffset } from '@opentrons/api-client' import { useMostRecentCompletedAnalysis } from '../../LabwarePositionCheck/useMostRecentCompletedAnalysis' diff --git a/app/src/organisms/Devices/RecentProtocolRuns.tsx b/app/src/organisms/Devices/RecentProtocolRuns.tsx index 4b07081e48d..558af301aaf 100644 --- a/app/src/organisms/Devices/RecentProtocolRuns.tsx +++ b/app/src/organisms/Devices/RecentProtocolRuns.tsx @@ -19,7 +19,7 @@ import { StyledText } from '../../atoms/text' import { useCurrentRunId } from '../ProtocolUpload/hooks' import { HistoricalProtocolRun } from './HistoricalProtocolRun' import { useIsRobotViewable, useRunStatuses } from './hooks' -import { useNotifyAllRunsQuery } from '../../resources/runs/useNotifyAllRunsQuery' +import { useNotifyAllRunsQuery } from '../../resources/runs' interface RecentProtocolRunsProps { robotName: string diff --git a/app/src/organisms/Devices/RobotSettings/AdvancedTab/AdvancedTabSlideouts/DeviceResetSlideout.tsx b/app/src/organisms/Devices/RobotSettings/AdvancedTab/AdvancedTabSlideouts/DeviceResetSlideout.tsx index f72db5e2671..6ca06ce941c 100644 --- a/app/src/organisms/Devices/RobotSettings/AdvancedTab/AdvancedTabSlideouts/DeviceResetSlideout.tsx +++ b/app/src/organisms/Devices/RobotSettings/AdvancedTab/AdvancedTabSlideouts/DeviceResetSlideout.tsx @@ -39,7 +39,7 @@ import { useTipLengthCalibrations, useRobot, } from '../../../hooks' -import { useNotifyAllRunsQuery } from '../../../../../resources/runs/useNotifyAllRunsQuery' +import { useNotifyAllRunsQuery } from '../../../../../resources/runs' import type { State, Dispatch } from '../../../../../redux/types' import type { ResetConfigRequest } from '../../../../../redux/robot-admin/types' diff --git a/app/src/organisms/Devices/RobotStatusHeader.tsx b/app/src/organisms/Devices/RobotStatusHeader.tsx index 224d2963809..e9dd34568ea 100644 --- a/app/src/organisms/Devices/RobotStatusHeader.tsx +++ b/app/src/organisms/Devices/RobotStatusHeader.tsx @@ -34,7 +34,7 @@ import { OPENTRONS_USB, } from '../../redux/discovery' import { getNetworkInterfaces, fetchStatus } from '../../redux/networking' -import { useNotifyRunQuery } from '../../resources/runs/useNotifyRunQuery' +import { useNotifyRunQuery } from '../../resources/runs' import type { IconName, StyleProps } from '@opentrons/components' import type { DiscoveredRobot } from '../../redux/discovery/types' diff --git a/app/src/organisms/Devices/__tests__/RecentProtocolRuns.test.tsx b/app/src/organisms/Devices/__tests__/RecentProtocolRuns.test.tsx index 5fcbbccadbe..aa4693135ed 100644 --- a/app/src/organisms/Devices/__tests__/RecentProtocolRuns.test.tsx +++ b/app/src/organisms/Devices/__tests__/RecentProtocolRuns.test.tsx @@ -4,7 +4,7 @@ import { screen } from '@testing-library/react' import { describe, it, vi, beforeEach } from 'vitest' import '@testing-library/jest-dom/vitest' import { renderWithProviders } from '../../../__testing-utils__' -import { useNotifyAllRunsQuery } from '../../../resources/runs/useNotifyAllRunsQuery' +import { useNotifyAllRunsQuery } from '../../../resources/runs' import { i18n } from '../../../i18n' import { useIsRobotViewable, useRunStatuses } from '../hooks' import { RecentProtocolRuns } from '../RecentProtocolRuns' @@ -13,7 +13,7 @@ import { HistoricalProtocolRun } from '../HistoricalProtocolRun' import type { Runs } from '@opentrons/api-client' import type { AxiosError } from 'axios' -vi.mock('../../../resources/runs/useNotifyAllRunsQuery') +vi.mock('../../../resources/runs') vi.mock('../hooks') vi.mock('../../ProtocolUpload/hooks') vi.mock('../HistoricalProtocolRun') diff --git a/app/src/organisms/Devices/__tests__/RobotStatusHeader.test.tsx b/app/src/organisms/Devices/__tests__/RobotStatusHeader.test.tsx index b1f21ae5839..38d73b9a944 100644 --- a/app/src/organisms/Devices/__tests__/RobotStatusHeader.test.tsx +++ b/app/src/organisms/Devices/__tests__/RobotStatusHeader.test.tsx @@ -19,7 +19,7 @@ import { import { getNetworkInterfaces } from '../../../redux/networking' import { useIsFlex } from '../hooks' import { RobotStatusHeader } from '../RobotStatusHeader' -import { useNotifyRunQuery } from '../../../resources/runs/useNotifyRunQuery' +import { useNotifyRunQuery } from '../../../resources/runs' import type { DiscoveryClientRobotAddress } from '../../../redux/discovery/types' import type { SimpleInterfaceStatus } from '../../../redux/networking/types' @@ -31,7 +31,7 @@ vi.mock('../../../organisms/RunTimeControl/hooks') vi.mock('../../../redux/discovery') vi.mock('../../../redux/networking') vi.mock('../hooks') -vi.mock('../../../resources/runs/useNotifyRunQuery') +vi.mock('../../../resources/runs') const MOCK_OTIE = { name: 'otie', diff --git a/app/src/organisms/Devices/hooks/__tests__/useIsRobotBusy.test.ts b/app/src/organisms/Devices/hooks/__tests__/useIsRobotBusy.test.ts index 77f06e074c9..457c0a75287 100644 --- a/app/src/organisms/Devices/hooks/__tests__/useIsRobotBusy.test.ts +++ b/app/src/organisms/Devices/hooks/__tests__/useIsRobotBusy.test.ts @@ -14,8 +14,8 @@ import { } from '../../../EmergencyStop' import { useIsRobotBusy } from '../useIsRobotBusy' import { useIsFlex } from '../useIsFlex' -import { useNotifyCurrentMaintenanceRun } from '../../../../resources/maintenance_runs/useNotifyCurrentMaintenanceRun' -import { useNotifyAllRunsQuery } from '../../../../resources/runs/useNotifyAllRunsQuery' +import { useNotifyCurrentMaintenanceRun } from '../../../../resources/maintenance_runs' +import { useNotifyAllRunsQuery } from '../../../../resources/runs' import type { Sessions, Runs } from '@opentrons/api-client' import type { AxiosError } from 'axios' @@ -23,8 +23,8 @@ import type { AxiosError } from 'axios' vi.mock('@opentrons/react-api-client') vi.mock('../../../ProtocolUpload/hooks') vi.mock('../useIsFlex') -vi.mock('../../../../resources/runs/useNotifyAllRunsQuery') -vi.mock('../../../../resources/maintenance_runs/useNotifyCurrentMaintenanceRun') +vi.mock('../../../../resources/runs') +vi.mock('../../../../resources/maintenance_runs') const mockEstopStatus = { data: { diff --git a/app/src/organisms/Devices/hooks/__tests__/useProtocolAnalysisErrors.test.tsx b/app/src/organisms/Devices/hooks/__tests__/useProtocolAnalysisErrors.test.tsx index 8fc7cff7d64..a327e420b05 100644 --- a/app/src/organisms/Devices/hooks/__tests__/useProtocolAnalysisErrors.test.tsx +++ b/app/src/organisms/Devices/hooks/__tests__/useProtocolAnalysisErrors.test.tsx @@ -9,9 +9,9 @@ import { } from '@opentrons/react-api-client' import { useProtocolAnalysisErrors } from '..' -import { useNotifyRunQuery } from '../../../../resources/runs/useNotifyRunQuery' +import { useNotifyRunQuery } from '../../../../resources/runs' -import { RUN_ID_2 } from '../../../../organisms/RunTimeControl/__fixtures__' +import { RUN_ID_2 } from '../../../RunTimeControl/__fixtures__' import type { Run, Protocol } from '@opentrons/api-client' import type { @@ -20,7 +20,7 @@ import type { } from '@opentrons/shared-data' vi.mock('@opentrons/react-api-client') -vi.mock('../../../../resources/runs/useNotifyRunQuery') +vi.mock('../../../../resources/runs') describe('useProtocolAnalysisErrors hook', () => { beforeEach(() => { diff --git a/app/src/organisms/Devices/hooks/__tests__/useProtocolDetailsForRun.test.tsx b/app/src/organisms/Devices/hooks/__tests__/useProtocolDetailsForRun.test.tsx index cf57b815dd7..7c0ad0363a9 100644 --- a/app/src/organisms/Devices/hooks/__tests__/useProtocolDetailsForRun.test.tsx +++ b/app/src/organisms/Devices/hooks/__tests__/useProtocolDetailsForRun.test.tsx @@ -9,9 +9,9 @@ import { } from '@opentrons/react-api-client' import { useProtocolDetailsForRun } from '..' -import { useNotifyRunQuery } from '../../../../resources/runs/useNotifyRunQuery' +import { useNotifyRunQuery } from '../../../../resources/runs' -import { RUN_ID_2 } from '../../../../organisms/RunTimeControl/__fixtures__' +import { RUN_ID_2 } from '../../../RunTimeControl/__fixtures__' import type { Protocol, Run } from '@opentrons/api-client' import { @@ -20,7 +20,7 @@ import { } from '@opentrons/shared-data' vi.mock('@opentrons/react-api-client') -vi.mock('../../../../resources/runs/useNotifyRunQuery') +vi.mock('../../../../resources/runs') const PROTOCOL_ID = 'fake_protocol_id' const PROTOCOL_ANALYSIS = { diff --git a/app/src/organisms/Devices/hooks/__tests__/useRunCalibrationStatus.test.tsx b/app/src/organisms/Devices/hooks/__tests__/useRunCalibrationStatus.test.tsx index 897dbd13394..a067332dd82 100644 --- a/app/src/organisms/Devices/hooks/__tests__/useRunCalibrationStatus.test.tsx +++ b/app/src/organisms/Devices/hooks/__tests__/useRunCalibrationStatus.test.tsx @@ -11,7 +11,7 @@ import { useIsFlex, useRunPipetteInfoByMount, } from '..' -import { useNotifyRunQuery } from '../../../../resources/runs/useNotifyRunQuery' +import { useNotifyRunQuery } from '../../../../resources/runs' import type { PipetteInfo } from '..' import { Provider } from 'react-redux' @@ -20,7 +20,7 @@ import { createStore } from 'redux' vi.mock('../useDeckCalibrationStatus') vi.mock('../useIsFlex') vi.mock('../useRunPipetteInfoByMount') -vi.mock('../../../../resources/runs/useNotifyRunQuery') +vi.mock('../../../../resources/runs') let wrapper: React.FunctionComponent<{ children: React.ReactNode }> diff --git a/app/src/organisms/Devices/hooks/__tests__/useRunCreatedAtTimestamp.test.tsx b/app/src/organisms/Devices/hooks/__tests__/useRunCreatedAtTimestamp.test.tsx index e4399c493db..07546e8b382 100644 --- a/app/src/organisms/Devices/hooks/__tests__/useRunCreatedAtTimestamp.test.tsx +++ b/app/src/organisms/Devices/hooks/__tests__/useRunCreatedAtTimestamp.test.tsx @@ -2,15 +2,15 @@ import { renderHook } from '@testing-library/react' import { vi, it, expect, describe, beforeEach } from 'vitest' import { when } from 'vitest-when' -import { mockIdleUnstartedRun } from '../../../../organisms/RunTimeControl/__fixtures__' +import { mockIdleUnstartedRun } from '../../../RunTimeControl/__fixtures__' import { formatTimestamp } from '../../utils' import { useRunCreatedAtTimestamp } from '../useRunCreatedAtTimestamp' -import { useNotifyRunQuery } from '../../../../resources/runs/useNotifyRunQuery' +import { useNotifyRunQuery } from '../../../../resources/runs' import type { UseQueryResult } from 'react-query' import type { Run } from '@opentrons/api-client' -vi.mock('../../../../resources/runs/useNotifyRunQuery') +vi.mock('../../../../resources/runs') vi.mock('../../utils') const MOCK_RUN_ID = '1' diff --git a/app/src/organisms/Devices/hooks/__tests__/useStoredProtocolAnalysis.test.tsx b/app/src/organisms/Devices/hooks/__tests__/useStoredProtocolAnalysis.test.tsx index 62275d66318..34365a075e7 100644 --- a/app/src/organisms/Devices/hooks/__tests__/useStoredProtocolAnalysis.test.tsx +++ b/app/src/organisms/Devices/hooks/__tests__/useStoredProtocolAnalysis.test.tsx @@ -25,14 +25,14 @@ import { PIPETTE_ENTITY, STORED_PROTOCOL_ANALYSIS, } from '../__fixtures__/storedProtocolAnalysis' -import { useNotifyRunQuery } from '../../../../resources/runs/useNotifyRunQuery' +import { useNotifyRunQuery } from '../../../../resources/runs' import type { Protocol, Run } from '@opentrons/api-client' vi.mock('@opentrons/api-client') vi.mock('@opentrons/react-api-client') vi.mock('../../../../redux/protocol-storage/selectors') -vi.mock('../../../../resources/runs/useNotifyRunQuery') +vi.mock('../../../../resources/runs') const store: Store = createStore(vi.fn(), {}) diff --git a/app/src/organisms/Devices/hooks/useIsRobotBusy.ts b/app/src/organisms/Devices/hooks/useIsRobotBusy.ts index 772039b22d2..671cfb39fcb 100644 --- a/app/src/organisms/Devices/hooks/useIsRobotBusy.ts +++ b/app/src/organisms/Devices/hooks/useIsRobotBusy.ts @@ -5,8 +5,8 @@ import { useCurrentAllSubsystemUpdatesQuery, } from '@opentrons/react-api-client' -import { useNotifyCurrentMaintenanceRun } from '../../../resources/maintenance_runs/useNotifyCurrentMaintenanceRun' -import { useNotifyAllRunsQuery } from '../../../resources/runs/useNotifyAllRunsQuery' +import { useNotifyCurrentMaintenanceRun } from '../../../resources/maintenance_runs' +import { useNotifyAllRunsQuery } from '../../../resources/runs' import { DISENGAGED } from '../../EmergencyStop' import { useIsFlex } from './useIsFlex' diff --git a/app/src/organisms/Devices/hooks/useProtocolAnalysisErrors.ts b/app/src/organisms/Devices/hooks/useProtocolAnalysisErrors.ts index 996c44989d4..1c86de6ecf5 100644 --- a/app/src/organisms/Devices/hooks/useProtocolAnalysisErrors.ts +++ b/app/src/organisms/Devices/hooks/useProtocolAnalysisErrors.ts @@ -4,7 +4,7 @@ import { useProtocolQuery, } from '@opentrons/react-api-client' -import { useNotifyRunQuery } from '../../../resources/runs/useNotifyRunQuery' +import { useNotifyRunQuery } from '../../../resources/runs' import { AnalysisError } from '@opentrons/shared-data' diff --git a/app/src/organisms/Devices/hooks/useProtocolDetailsForRun.ts b/app/src/organisms/Devices/hooks/useProtocolDetailsForRun.ts index f610b623d5c..57c50666488 100644 --- a/app/src/organisms/Devices/hooks/useProtocolDetailsForRun.ts +++ b/app/src/organisms/Devices/hooks/useProtocolDetailsForRun.ts @@ -6,7 +6,7 @@ import { useProtocolAnalysisAsDocumentQuery, } from '@opentrons/react-api-client' -import { useNotifyRunQuery } from '../../../resources/runs/useNotifyRunQuery' +import { useNotifyRunQuery } from '../../../resources/runs' import type { RobotType, diff --git a/app/src/organisms/Devices/hooks/useRunCreatedAtTimestamp.ts b/app/src/organisms/Devices/hooks/useRunCreatedAtTimestamp.ts index 03def4f2a4a..72936c75514 100644 --- a/app/src/organisms/Devices/hooks/useRunCreatedAtTimestamp.ts +++ b/app/src/organisms/Devices/hooks/useRunCreatedAtTimestamp.ts @@ -1,6 +1,6 @@ import { formatTimestamp } from '../utils' import { EMPTY_TIMESTAMP } from '../constants' -import { useNotifyRunQuery } from '../../../resources/runs/useNotifyRunQuery' +import { useNotifyRunQuery } from '../../../resources/runs' export function useRunCreatedAtTimestamp(runId: string | null): string { const runRecord = useNotifyRunQuery(runId) diff --git a/app/src/organisms/Devices/hooks/useStoredProtocolAnalysis.ts b/app/src/organisms/Devices/hooks/useStoredProtocolAnalysis.ts index 0a7571b1f6b..64b83e855c3 100644 --- a/app/src/organisms/Devices/hooks/useStoredProtocolAnalysis.ts +++ b/app/src/organisms/Devices/hooks/useStoredProtocolAnalysis.ts @@ -7,7 +7,7 @@ import { import { useProtocolQuery } from '@opentrons/react-api-client' import { getStoredProtocol } from '../../../redux/protocol-storage' -import { useNotifyRunQuery } from '../../../resources/runs/useNotifyRunQuery' +import { useNotifyRunQuery } from '../../../resources/runs' import type { ProtocolAnalysisOutput } from '@opentrons/shared-data' import type { State } from '../../../redux/types' diff --git a/app/src/organisms/DropTipWizard/__tests__/TipsAttachedModal.test.tsx b/app/src/organisms/DropTipWizard/__tests__/TipsAttachedModal.test.tsx index 0562efc9ae7..34540b1c516 100644 --- a/app/src/organisms/DropTipWizard/__tests__/TipsAttachedModal.test.tsx +++ b/app/src/organisms/DropTipWizard/__tests__/TipsAttachedModal.test.tsx @@ -10,12 +10,12 @@ import { handleTipsAttachedModal } from '../TipsAttachedModal' import { LEFT } from '@opentrons/shared-data' import { mockPipetteInfo } from '../../../redux/pipettes/__fixtures__' import { ROBOT_MODEL_OT3 } from '../../../redux/discovery' -import { useNotifyCurrentMaintenanceRun } from '../../../resources/maintenance_runs/useNotifyCurrentMaintenanceRun' +import { useNotifyCurrentMaintenanceRun } from '../../../resources/maintenance_runs' import type { PipetteModelSpecs } from '@opentrons/shared-data' import type { HostConfig } from '@opentrons/api-client' -vi.mock('../../../resources/maintenance_runs/useNotifyCurrentMaintenanceRun') +vi.mock('../../../resources/maintenance_runs') vi.mock('../../../resources/useNotifyService') const MOCK_ACTUAL_PIPETTE = { diff --git a/app/src/organisms/DropTipWizard/index.tsx b/app/src/organisms/DropTipWizard/index.tsx index 49396e76b4d..871ea158c0a 100644 --- a/app/src/organisms/DropTipWizard/index.tsx +++ b/app/src/organisms/DropTipWizard/index.tsx @@ -18,7 +18,7 @@ import { useDeckConfigurationQuery, } from '@opentrons/react-api-client' -import { useNotifyCurrentMaintenanceRun } from '../../resources/maintenance_runs/useNotifyCurrentMaintenanceRun' +import { useNotifyCurrentMaintenanceRun } from '../../resources/maintenance_runs' import { LegacyModalShell } from '../../molecules/LegacyModal' import { getTopPortalEl } from '../../App/portal' import { WizardHeader } from '../../molecules/WizardHeader' @@ -27,7 +27,7 @@ import { getIsOnDevice } from '../../redux/config' import { useChainMaintenanceCommands, useCreateTargetedMaintenanceRunMutation, -} from '../../resources/runs/hooks' +} from '../../resources/runs' import { StyledText } from '../../atoms/text' import { Jog } from '../../molecules/JogControls' import { ExitConfirmation } from './ExitConfirmation' diff --git a/app/src/organisms/FirmwareUpdateModal/FirmwareUpdateTakeover.tsx b/app/src/organisms/FirmwareUpdateModal/FirmwareUpdateTakeover.tsx index 47f9e82cb3f..33d581ea5e4 100644 --- a/app/src/organisms/FirmwareUpdateModal/FirmwareUpdateTakeover.tsx +++ b/app/src/organisms/FirmwareUpdateModal/FirmwareUpdateTakeover.tsx @@ -6,7 +6,7 @@ import { useCurrentAllSubsystemUpdatesQuery, useSubsystemUpdateQuery, } from '@opentrons/react-api-client' -import { useNotifyCurrentMaintenanceRun } from '../../resources/maintenance_runs/useNotifyCurrentMaintenanceRun' +import { useNotifyCurrentMaintenanceRun } from '../../resources/maintenance_runs' import { getTopPortalEl } from '../../App/portal' import { useIsUnboxingFlowOngoing } from '../RobotSettingsDashboard/NetworkSettings/hooks' import { UpdateInProgressModal } from './UpdateInProgressModal' diff --git a/app/src/organisms/FirmwareUpdateModal/__tests__/FirmwareUpdateTakeover.test.tsx b/app/src/organisms/FirmwareUpdateModal/__tests__/FirmwareUpdateTakeover.test.tsx index dd0aaa2e001..3816b85261f 100644 --- a/app/src/organisms/FirmwareUpdateModal/__tests__/FirmwareUpdateTakeover.test.tsx +++ b/app/src/organisms/FirmwareUpdateModal/__tests__/FirmwareUpdateTakeover.test.tsx @@ -14,7 +14,7 @@ import { UpdateNeededModal } from '../UpdateNeededModal' import { UpdateInProgressModal } from '../UpdateInProgressModal' import { useIsUnboxingFlowOngoing } from '../../RobotSettingsDashboard/NetworkSettings/hooks' import { FirmwareUpdateTakeover } from '../FirmwareUpdateTakeover' -import { useNotifyCurrentMaintenanceRun } from '../../../resources/maintenance_runs/useNotifyCurrentMaintenanceRun' +import { useNotifyCurrentMaintenanceRun } from '../../../resources/maintenance_runs' import type { BadPipette, PipetteData } from '@opentrons/api-client' @@ -22,7 +22,7 @@ vi.mock('@opentrons/react-api-client') vi.mock('../UpdateNeededModal') vi.mock('../UpdateInProgressModal') vi.mock('../../RobotSettingsDashboard/NetworkSettings/hooks') -vi.mock('../../../resources/maintenance_runs/useNotifyCurrentMaintenanceRun') +vi.mock('../../../resources/maintenance_runs') const render = () => { return renderWithProviders(, { diff --git a/app/src/organisms/GripperWizardFlows/MovePin.tsx b/app/src/organisms/GripperWizardFlows/MovePin.tsx index 736a97af275..61c156b43de 100644 --- a/app/src/organisms/GripperWizardFlows/MovePin.tsx +++ b/app/src/organisms/GripperWizardFlows/MovePin.tsx @@ -19,7 +19,7 @@ import calibratingFrontJaw from '../../assets/videos/gripper-wizards/CALIBRATING import calibratingRearJaw from '../../assets/videos/gripper-wizards/CALIBRATING_REAR_JAW.webm' import type { Coordinates } from '@opentrons/shared-data' -import type { CreateMaintenanceCommand } from '../../resources/runs/hooks' +import type { CreateMaintenanceCommand } from '../../resources/runs' import type { GripperWizardStepProps, MovePinStep } from './types' interface MovePinProps extends GripperWizardStepProps, MovePinStep { diff --git a/app/src/organisms/GripperWizardFlows/index.tsx b/app/src/organisms/GripperWizardFlows/index.tsx index 3c7b6d80b5b..8905b8ada7a 100644 --- a/app/src/organisms/GripperWizardFlows/index.tsx +++ b/app/src/organisms/GripperWizardFlows/index.tsx @@ -15,7 +15,7 @@ import { useCreateMaintenanceCommandMutation, useDeleteMaintenanceRunMutation, } from '@opentrons/react-api-client' -import { useNotifyCurrentMaintenanceRun } from '../../resources/maintenance_runs/useNotifyCurrentMaintenanceRun' +import { useNotifyCurrentMaintenanceRun } from '../../resources/maintenance_runs' import { LegacyModalShell } from '../../molecules/LegacyModal' import { getTopPortalEl } from '../../App/portal' import { WizardHeader } from '../../molecules/WizardHeader' @@ -24,7 +24,7 @@ import { getIsOnDevice } from '../../redux/config' import { useChainMaintenanceCommands, useCreateTargetedMaintenanceRunMutation, -} from '../../resources/runs/hooks' +} from '../../resources/runs' import { getGripperWizardSteps } from './getGripperWizardSteps' import { GRIPPER_FLOW_TYPES, SECTIONS } from './constants' import { BeforeBeginning } from './BeforeBeginning' diff --git a/app/src/organisms/InstrumentInfo/index.tsx b/app/src/organisms/InstrumentInfo/index.tsx index b850c9bd341..fe341e3c37f 100644 --- a/app/src/organisms/InstrumentInfo/index.tsx +++ b/app/src/organisms/InstrumentInfo/index.tsx @@ -21,7 +21,7 @@ import { StyledText } from '../../atoms/text' import { MediumButton } from '../../atoms/buttons' import { FLOWS } from '../PipetteWizardFlows/constants' import { GRIPPER_FLOW_TYPES } from '../GripperWizardFlows/constants' -import { formatTimeWithUtcLabel } from '../../resources/runs/utils' +import { formatTimeWithUtcLabel } from '../../resources/runs' import type { InstrumentData } from '@opentrons/api-client' import type { PipetteMount } from '@opentrons/shared-data' diff --git a/app/src/organisms/LabwarePositionCheck/AttachProbe.tsx b/app/src/organisms/LabwarePositionCheck/AttachProbe.tsx index f6756b8060d..de632137f09 100644 --- a/app/src/organisms/LabwarePositionCheck/AttachProbe.tsx +++ b/app/src/organisms/LabwarePositionCheck/AttachProbe.tsx @@ -13,7 +13,7 @@ import { RobotMotionLoader } from './RobotMotionLoader' import attachProbe1 from '../../assets/videos/pipette-wizard-flows/Pipette_Attach_Probe_1.webm' import attachProbe8 from '../../assets/videos/pipette-wizard-flows/Pipette_Attach_Probe_8.webm' import attachProbe96 from '../../assets/videos/pipette-wizard-flows/Pipette_Attach_Probe_96.webm' -import { useChainRunCommands } from '../../resources/runs/hooks' +import { useChainRunCommands } from '../../resources/runs' import { GenericWizardTile } from '../../molecules/GenericWizardTile' import type { Jog } from '../../molecules/JogControls/types' diff --git a/app/src/organisms/LabwarePositionCheck/CheckItem.tsx b/app/src/organisms/LabwarePositionCheck/CheckItem.tsx index 97fe3137690..dac9cbf3301 100644 --- a/app/src/organisms/LabwarePositionCheck/CheckItem.tsx +++ b/app/src/organisms/LabwarePositionCheck/CheckItem.tsx @@ -28,7 +28,7 @@ import { } from './utils/labware' import { UnorderedList } from '../../molecules/UnorderedList' import { getCurrentOffsetForLabwareInLocation } from '../Devices/ProtocolRun/utils/getCurrentOffsetForLabwareInLocation' -import { useChainRunCommands } from '../../resources/runs/hooks' +import { useChainRunCommands } from '../../resources/runs' import { getIsOnDevice } from '../../redux/config' import { getDisplayLocation } from './utils/getDisplayLocation' diff --git a/app/src/organisms/LabwarePositionCheck/DetachProbe.tsx b/app/src/organisms/LabwarePositionCheck/DetachProbe.tsx index a1681d90e17..a1278cd5673 100644 --- a/app/src/organisms/LabwarePositionCheck/DetachProbe.tsx +++ b/app/src/organisms/LabwarePositionCheck/DetachProbe.tsx @@ -11,7 +11,7 @@ import { import detachProbe1 from '../../assets/videos/pipette-wizard-flows/Pipette_Detach_Probe_1.webm' import detachProbe8 from '../../assets/videos/pipette-wizard-flows/Pipette_Detach_Probe_8.webm' import detachProbe96 from '../../assets/videos/pipette-wizard-flows/Pipette_Detach_Probe_96.webm' -import { useChainRunCommands } from '../../resources/runs/hooks' +import { useChainRunCommands } from '../../resources/runs' import { GenericWizardTile } from '../../molecules/GenericWizardTile' import type { Jog } from '../../molecules/JogControls/types' diff --git a/app/src/organisms/LabwarePositionCheck/IntroScreen/index.tsx b/app/src/organisms/LabwarePositionCheck/IntroScreen/index.tsx index e5e2a118d82..3a12db38c51 100644 --- a/app/src/organisms/LabwarePositionCheck/IntroScreen/index.tsx +++ b/app/src/organisms/LabwarePositionCheck/IntroScreen/index.tsx @@ -8,7 +8,7 @@ import { import { StyledText } from '../../../atoms/text' import { RobotMotionLoader } from '../RobotMotionLoader' import { getPrepCommands } from './getPrepCommands' -import { useChainRunCommands } from '../../../resources/runs/hooks' +import { useChainRunCommands } from '../../../resources/runs' import type { RegisterPositionAction } from '../types' import type { Jog } from '../../../molecules/JogControls' import { WizardRequiredEquipmentList } from '../../../molecules/WizardRequiredEquipmentList' diff --git a/app/src/organisms/LabwarePositionCheck/LabwarePositionCheckComponent.tsx b/app/src/organisms/LabwarePositionCheck/LabwarePositionCheckComponent.tsx index 2edb77616ad..440c6c89586 100644 --- a/app/src/organisms/LabwarePositionCheck/LabwarePositionCheckComponent.tsx +++ b/app/src/organisms/LabwarePositionCheck/LabwarePositionCheckComponent.tsx @@ -37,10 +37,10 @@ import { DetachProbe } from './DetachProbe' import { PickUpTip } from './PickUpTip' import { ReturnTip } from './ReturnTip' import { ResultsSummary } from './ResultsSummary' -import { useChainMaintenanceCommands } from '../../resources/runs/hooks' +import { useChainMaintenanceCommands } from '../../resources/runs' import { FatalErrorModal } from './FatalErrorModal' import { RobotMotionLoader } from './RobotMotionLoader' -import { useNotifyCurrentMaintenanceRun } from '../../resources/maintenance_runs/useNotifyCurrentMaintenanceRun' +import { useNotifyCurrentMaintenanceRun } from '../../resources/maintenance_runs' import { getLabwarePositionCheckSteps } from './getLabwarePositionCheckSteps' import type { Axis, Sign, StepSize } from '../../molecules/JogControls/types' diff --git a/app/src/organisms/LabwarePositionCheck/PickUpTip.tsx b/app/src/organisms/LabwarePositionCheck/PickUpTip.tsx index 72141eb28ae..5f1f8692f8c 100644 --- a/app/src/organisms/LabwarePositionCheck/PickUpTip.tsx +++ b/app/src/organisms/LabwarePositionCheck/PickUpTip.tsx @@ -18,7 +18,7 @@ import { MoveLabwareCreateCommand, RobotType, } from '@opentrons/shared-data' -import { useChainRunCommands } from '../../resources/runs/hooks' +import { useChainRunCommands } from '../../resources/runs' import { UnorderedList } from '../../molecules/UnorderedList' import { getCurrentOffsetForLabwareInLocation } from '../Devices/ProtocolRun/utils/getCurrentOffsetForLabwareInLocation' import { TipConfirmation } from './TipConfirmation' diff --git a/app/src/organisms/LabwarePositionCheck/ReturnTip.tsx b/app/src/organisms/LabwarePositionCheck/ReturnTip.tsx index a0c31074a5c..f4ecdf58154 100644 --- a/app/src/organisms/LabwarePositionCheck/ReturnTip.tsx +++ b/app/src/organisms/LabwarePositionCheck/ReturnTip.tsx @@ -12,7 +12,7 @@ import { } from '@opentrons/shared-data' import { StyledText } from '../../atoms/text' import { UnorderedList } from '../../molecules/UnorderedList' -import { useChainRunCommands } from '../../resources/runs/hooks' +import { useChainRunCommands } from '../../resources/runs' import { getLabwareDef, getLabwareDefinitionsFromCommands, diff --git a/app/src/organisms/LabwarePositionCheck/__tests__/useLaunchLPC.test.tsx b/app/src/organisms/LabwarePositionCheck/__tests__/useLaunchLPC.test.tsx index d4632045666..560a1bb70b1 100644 --- a/app/src/organisms/LabwarePositionCheck/__tests__/useLaunchLPC.test.tsx +++ b/app/src/organisms/LabwarePositionCheck/__tests__/useLaunchLPC.test.tsx @@ -19,9 +19,11 @@ import { import { FLEX_ROBOT_TYPE, fixtureTiprack300ul } from '@opentrons/shared-data' import { renderWithProviders } from '../../../__testing-utils__' -import { useCreateTargetedMaintenanceRunMutation } from '../../../resources/runs/hooks' +import { + useCreateTargetedMaintenanceRunMutation, + useNotifyRunQuery, +} from '../../../resources/runs' import { useMostRecentCompletedAnalysis } from '../useMostRecentCompletedAnalysis' -import { useNotifyRunQuery } from '../../../resources/runs/useNotifyRunQuery' import { useLaunchLPC } from '../useLaunchLPC' import { LabwarePositionCheck } from '..' @@ -31,9 +33,8 @@ import type { LabwareDefinition2 } from '@opentrons/shared-data' vi.mock('../') vi.mock('@opentrons/react-api-client') -vi.mock('../../../resources/runs/hooks') vi.mock('../useMostRecentCompletedAnalysis') -vi.mock('../../../resources/runs/useNotifyRunQuery') +vi.mock('../../../resources/runs') const MOCK_RUN_ID = 'mockRunId' const MOCK_MAINTENANCE_RUN_ID = 'mockMaintenanceRunId' diff --git a/app/src/organisms/LabwarePositionCheck/useLaunchLPC.tsx b/app/src/organisms/LabwarePositionCheck/useLaunchLPC.tsx index 3c24c90bfd9..d3a87ab91ee 100644 --- a/app/src/organisms/LabwarePositionCheck/useLaunchLPC.tsx +++ b/app/src/organisms/LabwarePositionCheck/useLaunchLPC.tsx @@ -5,11 +5,13 @@ import { useDeleteMaintenanceRunMutation, } from '@opentrons/react-api-client' -import { useCreateTargetedMaintenanceRunMutation } from '../../resources/runs/hooks' +import { + useCreateTargetedMaintenanceRunMutation, + useNotifyRunQuery, +} from '../../resources/runs' import { LabwarePositionCheck } from '.' import { useMostRecentCompletedAnalysis } from './useMostRecentCompletedAnalysis' import { getLabwareDefinitionsFromCommands } from './utils/labware' -import { useNotifyRunQuery } from '../../resources/runs/useNotifyRunQuery' import type { RobotType } from '@opentrons/shared-data' diff --git a/app/src/organisms/LabwarePositionCheck/useMostRecentCompletedAnalysis.ts b/app/src/organisms/LabwarePositionCheck/useMostRecentCompletedAnalysis.ts index 28d759466ab..0af8c075a58 100644 --- a/app/src/organisms/LabwarePositionCheck/useMostRecentCompletedAnalysis.ts +++ b/app/src/organisms/LabwarePositionCheck/useMostRecentCompletedAnalysis.ts @@ -4,7 +4,7 @@ import { useProtocolQuery, } from '@opentrons/react-api-client' -import { useNotifyRunQuery } from '../../resources/runs/useNotifyRunQuery' +import { useNotifyRunQuery } from '../../resources/runs' import type { CompletedProtocolAnalysis } from '@opentrons/shared-data' diff --git a/app/src/organisms/ModuleCard/index.tsx b/app/src/organisms/ModuleCard/index.tsx index 7066fe4f18d..28633c1595a 100644 --- a/app/src/organisms/ModuleCard/index.tsx +++ b/app/src/organisms/ModuleCard/index.tsx @@ -47,7 +47,7 @@ import { SUCCESS_TOAST } from '../../atoms/Toast' import { useMenuHandleClickOutside } from '../../atoms/MenuList/hooks' import { Tooltip } from '../../atoms/Tooltip' import { StyledText } from '../../atoms/text' -import { useChainLiveCommands } from '../../resources/runs/hooks' +import { useChainLiveCommands } from '../../resources/runs' import { useCurrentRunStatus } from '../RunTimeControl/hooks' import { useIsFlex } from '../../organisms/Devices/hooks' import { getModuleTooHot } from '../Devices/getModuleTooHot' diff --git a/app/src/organisms/ModuleWizardFlows/index.tsx b/app/src/organisms/ModuleWizardFlows/index.tsx index 8e3ff101c18..944e9bd27e3 100644 --- a/app/src/organisms/ModuleWizardFlows/index.tsx +++ b/app/src/organisms/ModuleWizardFlows/index.tsx @@ -23,7 +23,7 @@ import { useAttachedPipettesFromInstrumentsQuery } from '../../organisms/Devices import { useChainMaintenanceCommands, useCreateTargetedMaintenanceRunMutation, -} from '../../resources/runs/hooks' +} from '../../resources/runs' import { getIsOnDevice } from '../../redux/config' import { SimpleWizardBody } from '../../molecules/SimpleWizardBody' import { getModuleCalibrationSteps } from './getModuleCalibrationSteps' diff --git a/app/src/organisms/OnDeviceDisplay/RobotDashboard/__tests__/RecentRunProtocolCard.test.tsx b/app/src/organisms/OnDeviceDisplay/RobotDashboard/__tests__/RecentRunProtocolCard.test.tsx index cc869e1afb5..1cac85c3727 100644 --- a/app/src/organisms/OnDeviceDisplay/RobotDashboard/__tests__/RecentRunProtocolCard.test.tsx +++ b/app/src/organisms/OnDeviceDisplay/RobotDashboard/__tests__/RecentRunProtocolCard.test.tsx @@ -18,7 +18,7 @@ import { useTrackEvent } from '../../../../redux/analytics' import { useCloneRun } from '../../../ProtocolUpload/hooks' import { useHardwareStatusText } from '../hooks' import { RecentRunProtocolCard } from '../' -import { useNotifyAllRunsQuery } from '../../../../resources/runs/useNotifyAllRunsQuery' +import { useNotifyAllRunsQuery } from '../../../../resources/runs' import { useRobotInitializationStatus, INIT_STATUS, @@ -34,7 +34,7 @@ vi.mock('../../../../organisms/RunTimeControl/hooks') vi.mock('../../../../organisms/ProtocolUpload/hooks') vi.mock('../../../../redux/analytics') vi.mock('../hooks') -vi.mock('../../../../resources/runs/useNotifyAllRunsQuery') +vi.mock('../../../../resources/runs') vi.mock('../../../../resources/health/hooks') const RUN_ID = 'mockRunId' diff --git a/app/src/organisms/OnDeviceDisplay/RobotDashboard/__tests__/RecentRunProtocolCarousel.test.tsx b/app/src/organisms/OnDeviceDisplay/RobotDashboard/__tests__/RecentRunProtocolCarousel.test.tsx index 1015ee8cfac..85e956ed977 100644 --- a/app/src/organisms/OnDeviceDisplay/RobotDashboard/__tests__/RecentRunProtocolCarousel.test.tsx +++ b/app/src/organisms/OnDeviceDisplay/RobotDashboard/__tests__/RecentRunProtocolCarousel.test.tsx @@ -3,14 +3,14 @@ import { screen } from '@testing-library/react' import { beforeEach, describe, it, vi } from 'vitest' import { renderWithProviders } from '../../../../__testing-utils__' -import { useNotifyAllRunsQuery } from '../../../../resources/runs/useNotifyAllRunsQuery' +import { useNotifyAllRunsQuery } from '../../../../resources/runs' import { RecentRunProtocolCard, RecentRunProtocolCarousel } from '..' import type { RunData } from '@opentrons/api-client' vi.mock('@opentrons/react-api-client') vi.mock('../RecentRunProtocolCard') -vi.mock('../../../../resources/runs/useNotifyAllRunsQuery') +vi.mock('../../../../resources/runs') const mockRun = { actions: [], diff --git a/app/src/organisms/PipetteWizardFlows/index.tsx b/app/src/organisms/PipetteWizardFlows/index.tsx index 128a32896dd..1a671fb31fb 100644 --- a/app/src/organisms/PipetteWizardFlows/index.tsx +++ b/app/src/organisms/PipetteWizardFlows/index.tsx @@ -21,8 +21,8 @@ import { import { useCreateTargetedMaintenanceRunMutation, useChainMaintenanceCommands, -} from '../../resources/runs/hooks' -import { useNotifyCurrentMaintenanceRun } from '../../resources/maintenance_runs/useNotifyCurrentMaintenanceRun' +} from '../../resources/runs' +import { useNotifyCurrentMaintenanceRun } from '../../resources/maintenance_runs' import { LegacyModalShell } from '../../molecules/LegacyModal' import { getTopPortalEl } from '../../App/portal' import { WizardHeader } from '../../molecules/WizardHeader' diff --git a/app/src/organisms/ProtocolSetupModulesAndDeck/ModuleTable.tsx b/app/src/organisms/ProtocolSetupModulesAndDeck/ModuleTable.tsx index 5736ffe517b..e89b032c880 100644 --- a/app/src/organisms/ProtocolSetupModulesAndDeck/ModuleTable.tsx +++ b/app/src/organisms/ProtocolSetupModulesAndDeck/ModuleTable.tsx @@ -37,7 +37,7 @@ import { LocationConflictModal } from '../../organisms/Devices/ProtocolRun/Setup import { ModuleWizardFlows } from '../../organisms/ModuleWizardFlows' import { useToaster } from '../../organisms/ToasterOven' import { getLocalRobot } from '../../redux/discovery' -import { useChainLiveCommands } from '../../resources/runs/hooks' +import { useChainLiveCommands } from '../../resources/runs' import type { CommandData } from '@opentrons/api-client' import type { CutoutConfig, DeckDefinition } from '@opentrons/shared-data' diff --git a/app/src/organisms/ProtocolSetupModulesAndDeck/__tests__/ProtocolSetupModulesAndDeck.test.tsx b/app/src/organisms/ProtocolSetupModulesAndDeck/__tests__/ProtocolSetupModulesAndDeck.test.tsx index cd3250045d8..ead32d65d38 100644 --- a/app/src/organisms/ProtocolSetupModulesAndDeck/__tests__/ProtocolSetupModulesAndDeck.test.tsx +++ b/app/src/organisms/ProtocolSetupModulesAndDeck/__tests__/ProtocolSetupModulesAndDeck.test.tsx @@ -14,7 +14,7 @@ import { import { renderWithProviders } from '../../../__testing-utils__' import { i18n } from '../../../i18n' -import { useChainLiveCommands } from '../../../resources/runs/hooks' +import { useChainLiveCommands } from '../../../resources/runs' import { mockRobotSideAnalysis } from '../../CommandText/__fixtures__' import { useAttachedModules, @@ -40,7 +40,7 @@ import { ProtocolSetupModulesAndDeck } from '..' import type { CutoutConfig, DeckConfiguration } from '@opentrons/shared-data' vi.mock('@opentrons/react-api-client') -vi.mock('../../../resources/runs/hooks') +vi.mock('../../../resources/runs') vi.mock('../../../redux/discovery') vi.mock('../../../organisms/Devices/hooks') vi.mock( diff --git a/app/src/organisms/ProtocolUpload/hooks/__tests__/useCloneRun.test.tsx b/app/src/organisms/ProtocolUpload/hooks/__tests__/useCloneRun.test.tsx index 349e3633bf1..4f4fb33ab00 100644 --- a/app/src/organisms/ProtocolUpload/hooks/__tests__/useCloneRun.test.tsx +++ b/app/src/organisms/ProtocolUpload/hooks/__tests__/useCloneRun.test.tsx @@ -7,12 +7,12 @@ import { describe, it, beforeEach, afterEach, vi, expect } from 'vitest' import { useHost, useCreateRunMutation } from '@opentrons/react-api-client' import { useCloneRun } from '../useCloneRun' -import { useNotifyRunQuery } from '../../../../resources/runs/useNotifyRunQuery' +import { useNotifyRunQuery } from '../../../../resources/runs' import type { HostConfig } from '@opentrons/api-client' vi.mock('@opentrons/react-api-client') -vi.mock('../../../../resources/runs/useNotifyRunQuery') +vi.mock('../../../../resources/runs') const HOST_CONFIG: HostConfig = { hostname: 'localhost' } const RUN_ID: string = 'run_id' diff --git a/app/src/organisms/ProtocolUpload/hooks/__tests__/useCurrentRunId.test.tsx b/app/src/organisms/ProtocolUpload/hooks/__tests__/useCurrentRunId.test.tsx index 24f49066cd5..af4c9edf012 100644 --- a/app/src/organisms/ProtocolUpload/hooks/__tests__/useCurrentRunId.test.tsx +++ b/app/src/organisms/ProtocolUpload/hooks/__tests__/useCurrentRunId.test.tsx @@ -3,9 +3,9 @@ import { renderHook } from '@testing-library/react' import { describe, it, afterEach, expect, vi } from 'vitest' import { useCurrentRunId } from '../useCurrentRunId' -import { useNotifyAllRunsQuery } from '../../../../resources/runs/useNotifyAllRunsQuery' +import { useNotifyAllRunsQuery } from '../../../../resources/runs' -vi.mock('../../../../resources/runs/useNotifyAllRunsQuery') +vi.mock('../../../../resources/runs') describe('useCurrentRunId hook', () => { afterEach(() => { diff --git a/app/src/organisms/ProtocolUpload/hooks/__tests__/useMostRecentRunId.test.tsx b/app/src/organisms/ProtocolUpload/hooks/__tests__/useMostRecentRunId.test.tsx index f5bfe186884..e385b2d8f77 100644 --- a/app/src/organisms/ProtocolUpload/hooks/__tests__/useMostRecentRunId.test.tsx +++ b/app/src/organisms/ProtocolUpload/hooks/__tests__/useMostRecentRunId.test.tsx @@ -2,10 +2,10 @@ import { when } from 'vitest-when' import { renderHook } from '@testing-library/react' import { describe, it, afterEach, vi, expect } from 'vitest' -import { useNotifyAllRunsQuery } from '../../../../resources/runs/useNotifyAllRunsQuery' +import { useNotifyAllRunsQuery } from '../../../../resources/runs' import { useMostRecentRunId } from '../useMostRecentRunId' -vi.mock('../../../../resources/runs/useNotifyAllRunsQuery') +vi.mock('../../../../resources/runs') describe('useMostRecentRunId hook', () => { afterEach(() => { diff --git a/app/src/organisms/ProtocolUpload/hooks/useCloneRun.ts b/app/src/organisms/ProtocolUpload/hooks/useCloneRun.ts index 8512520d00f..c7ba887ab54 100644 --- a/app/src/organisms/ProtocolUpload/hooks/useCloneRun.ts +++ b/app/src/organisms/ProtocolUpload/hooks/useCloneRun.ts @@ -2,7 +2,7 @@ import { useQueryClient } from 'react-query' import { useHost, useCreateRunMutation } from '@opentrons/react-api-client' -import { useNotifyRunQuery } from '../../../resources/runs/useNotifyRunQuery' +import { useNotifyRunQuery } from '../../../resources/runs' import type { Run } from '@opentrons/api-client' diff --git a/app/src/organisms/ProtocolUpload/hooks/useCurrentRun.ts b/app/src/organisms/ProtocolUpload/hooks/useCurrentRun.ts index a1f1b288ddb..6510f7e672e 100644 --- a/app/src/organisms/ProtocolUpload/hooks/useCurrentRun.ts +++ b/app/src/organisms/ProtocolUpload/hooks/useCurrentRun.ts @@ -1,5 +1,5 @@ import { useCurrentRunId } from './useCurrentRunId' -import { useNotifyRunQuery } from '../../../resources/runs/useNotifyRunQuery' +import { useNotifyRunQuery } from '../../../resources/runs' import type { Run } from '@opentrons/api-client' diff --git a/app/src/organisms/ProtocolUpload/hooks/useCurrentRunId.ts b/app/src/organisms/ProtocolUpload/hooks/useCurrentRunId.ts index ad9f970b668..135ba73c504 100644 --- a/app/src/organisms/ProtocolUpload/hooks/useCurrentRunId.ts +++ b/app/src/organisms/ProtocolUpload/hooks/useCurrentRunId.ts @@ -1,4 +1,4 @@ -import { useNotifyAllRunsQuery } from '../../../resources/runs/useNotifyAllRunsQuery' +import { useNotifyAllRunsQuery } from '../../../resources/runs' import type { AxiosError } from 'axios' import type { UseAllRunsQueryOptions } from '@opentrons/react-api-client/src/runs/useAllRunsQuery' diff --git a/app/src/organisms/ProtocolUpload/hooks/useMostRecentRunId.ts b/app/src/organisms/ProtocolUpload/hooks/useMostRecentRunId.ts index 80dd694e905..f8f9898d170 100644 --- a/app/src/organisms/ProtocolUpload/hooks/useMostRecentRunId.ts +++ b/app/src/organisms/ProtocolUpload/hooks/useMostRecentRunId.ts @@ -1,6 +1,6 @@ import last from 'lodash/last' -import { useNotifyAllRunsQuery } from '../../../resources/runs/useNotifyAllRunsQuery' +import { useNotifyAllRunsQuery } from '../../../resources/runs' export function useMostRecentRunId(): string | null { const { data: allRuns } = useNotifyAllRunsQuery() diff --git a/app/src/organisms/RobotSettingsCalibration/CalibrationDetails/ModuleCalibrationOverflowMenu.tsx b/app/src/organisms/RobotSettingsCalibration/CalibrationDetails/ModuleCalibrationOverflowMenu.tsx index d1654558078..275e9490011 100644 --- a/app/src/organisms/RobotSettingsCalibration/CalibrationDetails/ModuleCalibrationOverflowMenu.tsx +++ b/app/src/organisms/RobotSettingsCalibration/CalibrationDetails/ModuleCalibrationOverflowMenu.tsx @@ -16,7 +16,7 @@ import { import { Tooltip } from '../../../atoms/Tooltip' import { OverflowBtn } from '../../../atoms/MenuList/OverflowBtn' import { MenuItem } from '../../../atoms/MenuList/MenuItem' -import { useChainLiveCommands } from '../../../resources/runs/hooks' +import { useChainLiveCommands } from '../../../resources/runs' import { useMenuHandleClickOutside } from '../../../atoms/MenuList/hooks' import { useRunStatuses } from '../../Devices/hooks' import { getModulePrepCommands } from '../../Devices/getModulePrepCommands' diff --git a/app/src/organisms/RobotSettingsCalibration/CalibrationDetails/__tests__/ModuleCalibrationOverflowMenu.test.tsx b/app/src/organisms/RobotSettingsCalibration/CalibrationDetails/__tests__/ModuleCalibrationOverflowMenu.test.tsx index 4528c6bfe7b..44bcb21836c 100644 --- a/app/src/organisms/RobotSettingsCalibration/CalibrationDetails/__tests__/ModuleCalibrationOverflowMenu.test.tsx +++ b/app/src/organisms/RobotSettingsCalibration/CalibrationDetails/__tests__/ModuleCalibrationOverflowMenu.test.tsx @@ -5,7 +5,7 @@ import { describe, it, expect, vi, beforeEach } from 'vitest' import { i18n } from '../../../../i18n' import { renderWithProviders } from '../../../../__testing-utils__' import { ModuleWizardFlows } from '../../../ModuleWizardFlows' -import { useChainLiveCommands } from '../../../../resources/runs/hooks' +import { useChainLiveCommands } from '../../../../resources/runs' import { mockThermocyclerGen2 } from '../../../../redux/modules/__fixtures__' import { useRunStatuses } from '../../../Devices/hooks' import { useIsEstopNotDisengaged } from '../../../../resources/devices/hooks/useIsEstopNotDisengaged' @@ -17,7 +17,7 @@ import type { Mount } from '@opentrons/components' vi.mock('@opentrons/react-api-client') vi.mock('../../../ModuleWizardFlows') vi.mock('../../../Devices/hooks') -vi.mock('../../../../resources/runs/hooks') +vi.mock('../../../../resources/runs') vi.mock('../../../../resources/devices/hooks/useIsEstopNotDisengaged') const mockPipetteOffsetCalibrations = [ diff --git a/app/src/organisms/RunPreview/index.tsx b/app/src/organisms/RunPreview/index.tsx index 605db840cc9..1a27fea26d2 100644 --- a/app/src/organisms/RunPreview/index.tsx +++ b/app/src/organisms/RunPreview/index.tsx @@ -19,7 +19,7 @@ import { import { StyledText } from '../../atoms/text' import { useMostRecentCompletedAnalysis } from '../LabwarePositionCheck/useMostRecentCompletedAnalysis' -import { useNotifyLastRunCommandKey } from '../../resources/runs/useNotifyLastRunCommandKey' +import { useNotifyLastRunCommandKey } from '../../resources/runs' import { CommandText } from '../CommandText' import { Divider } from '../../atoms/structure' import { NAV_BAR_WIDTH } from '../../App/constants' diff --git a/app/src/organisms/RunProgressMeter/__tests__/RunProgressMeter.test.tsx b/app/src/organisms/RunProgressMeter/__tests__/RunProgressMeter.test.tsx index d4657b06174..aba56366b27 100644 --- a/app/src/organisms/RunProgressMeter/__tests__/RunProgressMeter.test.tsx +++ b/app/src/organisms/RunProgressMeter/__tests__/RunProgressMeter.test.tsx @@ -18,7 +18,10 @@ import { InterventionModal } from '../../InterventionModal' import { ProgressBar } from '../../../atoms/ProgressBar' import { useRunStatus } from '../../RunTimeControl/hooks' import { useMostRecentCompletedAnalysis } from '../../LabwarePositionCheck/useMostRecentCompletedAnalysis' -import { useNotifyLastRunCommandKey } from '../../../resources/runs/useNotifyLastRunCommandKey' +import { + useNotifyLastRunCommandKey, + useNotifyRunQuery, +} from '../../../resources/runs' import { useDownloadRunLog } from '../../Devices/hooks' import { mockUseAllCommandsResponseNonDeterministic, @@ -31,7 +34,6 @@ import { mockRunData, } from '../../InterventionModal/__fixtures__' import { RunProgressMeter } from '..' -import { useNotifyRunQuery } from '../../../resources/runs/useNotifyRunQuery' import { renderWithProviders } from '../../../__testing-utils__' import type * as ApiClient from '@opentrons/react-api-client' @@ -45,11 +47,10 @@ vi.mock('@opentrons/react-api-client', async importOriginal => { }) vi.mock('../../RunTimeControl/hooks') vi.mock('../../LabwarePositionCheck/useMostRecentCompletedAnalysis') -vi.mock('../../../resources/runs/useNotifyLastRunCommandKey') +vi.mock('../../../resources/runs') vi.mock('../../Devices/hooks') vi.mock('../../../atoms/ProgressBar') vi.mock('../../InterventionModal') -vi.mock('../../../resources/runs/useNotifyRunQuery') const render = (props: React.ComponentProps) => { return renderWithProviders(, { diff --git a/app/src/organisms/RunProgressMeter/index.tsx b/app/src/organisms/RunProgressMeter/index.tsx index 532862dff91..fed3d864f18 100644 --- a/app/src/organisms/RunProgressMeter/index.tsx +++ b/app/src/organisms/RunProgressMeter/index.tsx @@ -42,7 +42,7 @@ import { ProgressBar } from '../../atoms/ProgressBar' import { useDownloadRunLog, useRobotType } from '../Devices/hooks' import { InterventionTicks } from './InterventionTicks' import { isInterventionCommand } from '../InterventionModal/utils' -import { useNotifyRunQuery } from '../../resources/runs/useNotifyRunQuery' +import { useNotifyRunQuery } from '../../resources/runs' import type { RunStatus } from '@opentrons/api-client' diff --git a/app/src/organisms/RunTimeControl/__tests__/hooks.test.tsx b/app/src/organisms/RunTimeControl/__tests__/hooks.test.tsx index 79a631aef6e..21adedbd165 100644 --- a/app/src/organisms/RunTimeControl/__tests__/hooks.test.tsx +++ b/app/src/organisms/RunTimeControl/__tests__/hooks.test.tsx @@ -17,7 +17,7 @@ import { useRunTimestamps, useRunErrors, } from '../hooks' -import { useNotifyRunQuery } from '../../../resources/runs/useNotifyRunQuery' +import { useNotifyRunQuery } from '../../../resources/runs' import { RUN_ID_2, @@ -43,7 +43,7 @@ vi.mock('@opentrons/react-api-client', async importOriginal => { }) vi.mock('../../ProtocolUpload/hooks') -vi.mock('../../../resources/runs/useNotifyRunQuery') +vi.mock('../../../resources/runs') describe('useRunControls hook', () => { it('returns run controls hooks', () => { diff --git a/app/src/organisms/RunTimeControl/hooks.ts b/app/src/organisms/RunTimeControl/hooks.ts index 1c676077d98..e7a961e558a 100644 --- a/app/src/organisms/RunTimeControl/hooks.ts +++ b/app/src/organisms/RunTimeControl/hooks.ts @@ -20,7 +20,7 @@ import { useCurrentRunId, useRunCommands, } from '../ProtocolUpload/hooks' -import { useNotifyRunQuery } from '../../resources/runs/useNotifyRunQuery' +import { useNotifyRunQuery } from '../../resources/runs' import type { UseQueryOptions } from 'react-query' import type { RunAction, RunStatus, Run, RunData } from '@opentrons/api-client' diff --git a/app/src/organisms/SendProtocolToFlexSlideout/__tests__/SendProtocolToFlexSlideout.test.tsx b/app/src/organisms/SendProtocolToFlexSlideout/__tests__/SendProtocolToFlexSlideout.test.tsx index 0e33a4a2807..9f5279aa18f 100644 --- a/app/src/organisms/SendProtocolToFlexSlideout/__tests__/SendProtocolToFlexSlideout.test.tsx +++ b/app/src/organisms/SendProtocolToFlexSlideout/__tests__/SendProtocolToFlexSlideout.test.tsx @@ -34,7 +34,7 @@ import { getNetworkInterfaces } from '../../../redux/networking' import { getIsProtocolAnalysisInProgress } from '../../../redux/protocol-storage/selectors' import { storedProtocolData as storedProtocolDataFixture } from '../../../redux/protocol-storage/__fixtures__' import { SendProtocolToFlexSlideout } from '..' -import { useNotifyAllRunsQuery } from '../../../resources/runs/useNotifyAllRunsQuery' +import { useNotifyAllRunsQuery } from '../../../resources/runs' import type * as ApiClient from '@opentrons/react-api-client' @@ -51,7 +51,7 @@ vi.mock('../../../redux/discovery') vi.mock('../../../redux/networking') vi.mock('../../../redux/custom-labware') vi.mock('../../../redux/protocol-storage/selectors') -vi.mock('../../../resources/runs/useNotifyAllRunsQuery') +vi.mock('../../../resources/runs') const render = ( props: React.ComponentProps diff --git a/app/src/organisms/TakeoverModal/MaintenanceRunStatusProvider.tsx b/app/src/organisms/TakeoverModal/MaintenanceRunStatusProvider.tsx index fcc7c3c73fe..46b2062de39 100644 --- a/app/src/organisms/TakeoverModal/MaintenanceRunStatusProvider.tsx +++ b/app/src/organisms/TakeoverModal/MaintenanceRunStatusProvider.tsx @@ -1,6 +1,6 @@ import * as React from 'react' -import { useNotifyCurrentMaintenanceRun } from '../../resources/maintenance_runs/useNotifyCurrentMaintenanceRun' +import { useNotifyCurrentMaintenanceRun } from '../../resources/maintenance_runs' interface MaintenanceRunIds { currentRunId: string | null diff --git a/app/src/organisms/TakeoverModal/__tests__/MaintenanceRunTakeover.test.tsx b/app/src/organisms/TakeoverModal/__tests__/MaintenanceRunTakeover.test.tsx index f4416fa8a9f..0b236577a97 100644 --- a/app/src/organisms/TakeoverModal/__tests__/MaintenanceRunTakeover.test.tsx +++ b/app/src/organisms/TakeoverModal/__tests__/MaintenanceRunTakeover.test.tsx @@ -6,12 +6,12 @@ import { i18n } from '../../../i18n' import { renderWithProviders } from '../../../__testing-utils__' import { useMaintenanceRunTakeover } from '../useMaintenanceRunTakeover' import { MaintenanceRunTakeover } from '../MaintenanceRunTakeover' -import { useNotifyCurrentMaintenanceRun } from '../../../resources/maintenance_runs/useNotifyCurrentMaintenanceRun' +import { useNotifyCurrentMaintenanceRun } from '../../../resources/maintenance_runs' import type { MaintenanceRunStatus } from '../MaintenanceRunStatusProvider' vi.mock('../useMaintenanceRunTakeover') -vi.mock('../../../resources/maintenance_runs/useNotifyCurrentMaintenanceRun') +vi.mock('../../../resources/maintenance_runs') const MOCK_MAINTENANCE_RUN: MaintenanceRunStatus = { getRunIds: () => ({ diff --git a/app/src/pages/Devices/CalibrationDashboard/__tests__/CalibrationDashboard.test.tsx b/app/src/pages/Devices/CalibrationDashboard/__tests__/CalibrationDashboard.test.tsx index 46229d23cfa..1c46e097c41 100644 --- a/app/src/pages/Devices/CalibrationDashboard/__tests__/CalibrationDashboard.test.tsx +++ b/app/src/pages/Devices/CalibrationDashboard/__tests__/CalibrationDashboard.test.tsx @@ -16,13 +16,13 @@ import { useDashboardCalibrateTipLength } from '../hooks/useDashboardCalibrateTi import { useDashboardCalibrateDeck } from '../hooks/useDashboardCalibrateDeck' import { expectedTaskList } from '../../../../organisms/Devices/hooks/__fixtures__/taskListFixtures' import { mockLeftProtoPipette } from '../../../../redux/pipettes/__fixtures__' -import { useNotifyAllRunsQuery } from '../../../../resources/runs/useNotifyAllRunsQuery' +import { useNotifyAllRunsQuery } from '../../../../resources/runs' vi.mock('../../../../organisms/Devices/hooks') vi.mock('../hooks/useDashboardCalibratePipOffset') vi.mock('../hooks/useDashboardCalibrateTipLength') vi.mock('../hooks/useDashboardCalibrateDeck') -vi.mock('../../../../resources/runs/useNotifyAllRunsQuery') +vi.mock('../../../../resources/runs') const render = (path = '/') => { return renderWithProviders( diff --git a/app/src/pages/InstrumentDetail/__tests__/InstrumentDetailOverflowMenu.test.tsx b/app/src/pages/InstrumentDetail/__tests__/InstrumentDetailOverflowMenu.test.tsx index 40095e581d2..9a6e797b851 100644 --- a/app/src/pages/InstrumentDetail/__tests__/InstrumentDetailOverflowMenu.test.tsx +++ b/app/src/pages/InstrumentDetail/__tests__/InstrumentDetailOverflowMenu.test.tsx @@ -8,7 +8,7 @@ import { getPipetteModelSpecs } from '@opentrons/shared-data' import { i18n } from '../../../i18n' import { handleInstrumentDetailOverflowMenu } from '../InstrumentDetailOverflowMenu' -import { useNotifyCurrentMaintenanceRun } from '../../../resources/maintenance_runs/useNotifyCurrentMaintenanceRun' +import { useNotifyCurrentMaintenanceRun } from '../../../resources/maintenance_runs' import { PipetteWizardFlows } from '../../../organisms/PipetteWizardFlows' import { GripperWizardFlows } from '../../../organisms/GripperWizardFlows' import { DropTipWizard } from '../../../organisms/DropTipWizard' @@ -27,7 +27,7 @@ vi.mock('@opentrons/shared-data', async importOriginal => { getPipetteModelSpecs: vi.fn(), } }) -vi.mock('../../../resources/maintenance_runs/useNotifyCurrentMaintenanceRun') +vi.mock('../../../resources/maintenance_runs') vi.mock('../../../organisms/PipetteWizardFlows') vi.mock('../../../organisms/GripperWizardFlows') vi.mock('../../../organisms/DropTipWizard') diff --git a/app/src/pages/InstrumentsDashboard/__tests__/InstrumentsDashboard.test.tsx b/app/src/pages/InstrumentsDashboard/__tests__/InstrumentsDashboard.test.tsx index 0dc938b663a..d816731eea1 100644 --- a/app/src/pages/InstrumentsDashboard/__tests__/InstrumentsDashboard.test.tsx +++ b/app/src/pages/InstrumentsDashboard/__tests__/InstrumentsDashboard.test.tsx @@ -9,7 +9,7 @@ import { i18n } from '../../../i18n' import { ChoosePipette } from '../../../organisms/PipetteWizardFlows/ChoosePipette' import { GripperWizardFlows } from '../../../organisms/GripperWizardFlows' import { InstrumentsDashboard } from '..' -import { formatTimeWithUtcLabel } from '../../../resources/runs/utils' +import { formatTimeWithUtcLabel } from '../../../resources/runs' import { InstrumentDetail } from '../../../pages/InstrumentDetail' import type * as ReactApiClient from '@opentrons/react-api-client' diff --git a/app/src/pages/ProtocolDashboard/PinnedProtocol.tsx b/app/src/pages/ProtocolDashboard/PinnedProtocol.tsx index 9fe60365cf3..269c087b1ac 100644 --- a/app/src/pages/ProtocolDashboard/PinnedProtocol.tsx +++ b/app/src/pages/ProtocolDashboard/PinnedProtocol.tsx @@ -20,7 +20,7 @@ import { import { StyledText } from '../../atoms/text' import { LongPressModal } from './LongPressModal' -import { formatTimeWithUtcLabel } from '../../resources/runs/utils' +import { formatTimeWithUtcLabel } from '../../resources/runs' import type { UseLongPressResult } from '@opentrons/components' import type { ProtocolResource } from '@opentrons/shared-data' diff --git a/app/src/pages/ProtocolDashboard/PinnedProtocolCarousel.tsx b/app/src/pages/ProtocolDashboard/PinnedProtocolCarousel.tsx index 7932f40ee15..3f39aefcdf9 100644 --- a/app/src/pages/ProtocolDashboard/PinnedProtocolCarousel.tsx +++ b/app/src/pages/ProtocolDashboard/PinnedProtocolCarousel.tsx @@ -6,7 +6,7 @@ import { SPACING, } from '@opentrons/components' -import { useNotifyAllRunsQuery } from '../../resources/runs/useNotifyAllRunsQuery' +import { useNotifyAllRunsQuery } from '../../resources/runs' import { PinnedProtocol } from './PinnedProtocol' import type { ProtocolResource } from '@opentrons/shared-data' diff --git a/app/src/pages/ProtocolDashboard/ProtocolCard.tsx b/app/src/pages/ProtocolDashboard/ProtocolCard.tsx index 9aeab42cb76..bc630b5dd3a 100644 --- a/app/src/pages/ProtocolDashboard/ProtocolCard.tsx +++ b/app/src/pages/ProtocolDashboard/ProtocolCard.tsx @@ -32,7 +32,7 @@ import { StyledText } from '../../atoms/text' import { SmallButton } from '../../atoms/buttons' import { Modal } from '../../molecules/Modal' import { LongPressModal } from './LongPressModal' -import { formatTimeWithUtcLabel } from '../../resources/runs/utils' +import { formatTimeWithUtcLabel } from '../../resources/runs' import type { UseLongPressResult } from '@opentrons/components' import type { ProtocolResource } from '@opentrons/shared-data' diff --git a/app/src/pages/ProtocolDashboard/index.tsx b/app/src/pages/ProtocolDashboard/index.tsx index e27d18da0f7..2fce4e5b988 100644 --- a/app/src/pages/ProtocolDashboard/index.tsx +++ b/app/src/pages/ProtocolDashboard/index.tsx @@ -28,7 +28,7 @@ import { sortProtocols } from './utils' import { ProtocolCard } from './ProtocolCard' import { NoProtocols } from './NoProtocols' import { DeleteProtocolConfirmationModal } from './DeleteProtocolConfirmationModal' -import { useNotifyAllRunsQuery } from '../../resources/runs/useNotifyAllRunsQuery' +import { useNotifyAllRunsQuery } from '../../resources/runs' import type { Dispatch } from '../../redux/types' import type { ProtocolsOnDeviceSortKey } from '../../redux/config/types' diff --git a/app/src/pages/ProtocolDetails/__tests__/ProtocolDetails.test.tsx b/app/src/pages/ProtocolDetails/__tests__/ProtocolDetails.test.tsx index 004a31ef865..1c44e41685e 100644 --- a/app/src/pages/ProtocolDetails/__tests__/ProtocolDetails.test.tsx +++ b/app/src/pages/ProtocolDetails/__tests__/ProtocolDetails.test.tsx @@ -21,7 +21,7 @@ import { i18n } from '../../../i18n' import { useHardwareStatusText } from '../../../organisms/OnDeviceDisplay/RobotDashboard/hooks' import { useOffsetCandidatesForAnalysis } from '../../../organisms/ApplyHistoricOffsets/hooks/useOffsetCandidatesForAnalysis' import { useMissingProtocolHardware } from '../../Protocols/hooks' -import { formatTimeWithUtcLabel } from '../../../resources/runs/utils' +import { formatTimeWithUtcLabel } from '../../../resources/runs' import { ProtocolDetails } from '..' import { Deck } from '../Deck' import { Hardware } from '../Hardware' diff --git a/app/src/pages/ProtocolDetails/index.tsx b/app/src/pages/ProtocolDetails/index.tsx index 43e35739e31..806332e624b 100644 --- a/app/src/pages/ProtocolDetails/index.tsx +++ b/app/src/pages/ProtocolDetails/index.tsx @@ -50,7 +50,7 @@ import { Deck } from './Deck' import { Hardware } from './Hardware' import { Labware } from './Labware' import { Liquids } from './Liquids' -import { formatTimeWithUtcLabel } from '../../resources/runs/utils' +import { formatTimeWithUtcLabel } from '../../resources/runs' import type { Protocol } from '@opentrons/api-client' import type { ModalHeaderBaseProps } from '../../molecules/Modal/types' diff --git a/app/src/pages/ProtocolSetup/__tests__/ProtocolSetup.test.tsx b/app/src/pages/ProtocolSetup/__tests__/ProtocolSetup.test.tsx index bd14dc90f1d..11906d3d1b8 100644 --- a/app/src/pages/ProtocolSetup/__tests__/ProtocolSetup.test.tsx +++ b/app/src/pages/ProtocolSetup/__tests__/ProtocolSetup.test.tsx @@ -51,7 +51,7 @@ import { useIsHeaterShakerInProtocol } from '../../../organisms/ModuleCard/hooks import { useDeckConfigurationCompatibility } from '../../../resources/deck_configuration/hooks' import { ConfirmAttachedModal } from '../../../pages/ProtocolSetup/ConfirmAttachedModal' import { ProtocolSetup } from '../../../pages/ProtocolSetup' -import { useNotifyRunQuery } from '../../../resources/runs/useNotifyRunQuery' +import { useNotifyRunQuery } from '../../../resources/runs' import { mockConnectableRobot } from '../../../redux/discovery/__fixtures__' import type { UseQueryResult } from 'react-query' @@ -107,7 +107,7 @@ vi.mock('../../../redux/discovery/selectors') vi.mock('../ConfirmAttachedModal') vi.mock('../../../organisms/ToasterOven') vi.mock('../../../resources/deck_configuration/hooks') -vi.mock('../../../resources/runs/useNotifyRunQuery') +vi.mock('../../../resources/runs') const render = (path = '/') => { return renderWithProviders( diff --git a/app/src/pages/ProtocolSetup/index.tsx b/app/src/pages/ProtocolSetup/index.tsx index 98c29a987dd..cfca080b343 100644 --- a/app/src/pages/ProtocolSetup/index.tsx +++ b/app/src/pages/ProtocolSetup/index.tsx @@ -54,7 +54,7 @@ import { import { useRequiredProtocolHardwareFromAnalysis, useMissingProtocolHardwareFromAnalysis, -} from '../../pages/Protocols/hooks' +} from '../Protocols/hooks' import { getProtocolModulesInfo } from '../../organisms/Devices/ProtocolRun/utils/getProtocolModulesInfo' import { ProtocolSetupLabware } from '../../organisms/ProtocolSetupLabware' import { ProtocolSetupModulesAndDeck } from '../../organisms/ProtocolSetupModulesAndDeck' @@ -74,7 +74,7 @@ import { } from '../../organisms/RunTimeControl/hooks' import { useToaster } from '../../organisms/ToasterOven' import { useIsHeaterShakerInProtocol } from '../../organisms/ModuleCard/hooks' -import { getLabwareSetupItemGroups } from '../../pages/Protocols/utils' +import { getLabwareSetupItemGroups } from '../Protocols/utils' import { getLocalRobot, getRobotSerialNumber } from '../../redux/discovery' import { ANALYTICS_PROTOCOL_PROCEED_TO_RUN, @@ -82,12 +82,12 @@ import { useTrackEvent, } from '../../redux/analytics' import { getIsHeaterShakerAttached } from '../../redux/config' -import { ConfirmAttachedModal } from '../../pages/ProtocolSetup/ConfirmAttachedModal' +import { ConfirmAttachedModal } from './ConfirmAttachedModal' import { getLatestCurrentOffsets } from '../../organisms/Devices/ProtocolRun/SetupLabwarePositionCheck/utils' -import { CloseButton, PlayButton } from '../../pages/ProtocolSetup/Buttons' +import { CloseButton, PlayButton } from './Buttons' import { useDeckConfigurationCompatibility } from '../../resources/deck_configuration/hooks' import { getRequiredDeckConfig } from '../../resources/deck_configuration/utils' -import { useNotifyRunQuery } from '../../resources/runs/useNotifyRunQuery' +import { useNotifyRunQuery } from '../../resources/runs' import type { CutoutFixtureId, CutoutId } from '@opentrons/shared-data' import type { OnDeviceRouteParams } from '../../App/types' diff --git a/app/src/pages/RobotDashboard/__tests__/RobotDashboard.test.tsx b/app/src/pages/RobotDashboard/__tests__/RobotDashboard.test.tsx index a5e0c58fa93..7706a925826 100644 --- a/app/src/pages/RobotDashboard/__tests__/RobotDashboard.test.tsx +++ b/app/src/pages/RobotDashboard/__tests__/RobotDashboard.test.tsx @@ -6,14 +6,16 @@ import { renderWithProviders } from '../../../__testing-utils__' import { useAllProtocolsQuery } from '@opentrons/react-api-client' import { i18n } from '../../../i18n' -import { EmptyRecentRun } from '../../../organisms/OnDeviceDisplay/RobotDashboard/EmptyRecentRun' -import { RecentRunProtocolCarousel } from '../../../organisms/OnDeviceDisplay/RobotDashboard' +import { + RecentRunProtocolCarousel, + EmptyRecentRun, +} from '../../../organisms/OnDeviceDisplay/RobotDashboard' import { Navigation } from '../../../organisms/Navigation' import { useMissingProtocolHardware } from '../../Protocols/hooks' import { getOnDeviceDisplaySettings } from '../../../redux/config' import { WelcomeModal } from '../WelcomeModal' import { RobotDashboard } from '..' -import { useNotifyAllRunsQuery } from '../../../resources/runs/useNotifyAllRunsQuery' +import { useNotifyAllRunsQuery } from '../../../resources/runs' import type { ProtocolResource } from '@opentrons/shared-data' import type * as ReactRouterDom from 'react-router-dom' @@ -36,7 +38,7 @@ vi.mock('../../../organisms/Navigation') vi.mock('../../Protocols/hooks') vi.mock('../../../redux/config') vi.mock('../WelcomeModal') -vi.mock('../../../resources/runs/useNotifyAllRunsQuery') +vi.mock('../../../resources/runs') const render = () => { return renderWithProviders( diff --git a/app/src/pages/RobotDashboard/index.tsx b/app/src/pages/RobotDashboard/index.tsx index 5b3b462481f..e0b699dea4b 100644 --- a/app/src/pages/RobotDashboard/index.tsx +++ b/app/src/pages/RobotDashboard/index.tsx @@ -21,7 +21,7 @@ import { AnalyticsOptInModal } from './AnalyticsOptInModal' import { WelcomeModal } from './WelcomeModal' import { RunData } from '@opentrons/api-client' import { ServerInitializing } from '../../organisms/OnDeviceDisplay/RobotDashboard/ServerInitializing' -import { useNotifyAllRunsQuery } from '../../resources/runs/useNotifyAllRunsQuery' +import { useNotifyAllRunsQuery } from '../../resources/runs' export const MAXIMUM_RECENT_RUN_PROTOCOLS = 8 diff --git a/app/src/pages/RunSummary/index.tsx b/app/src/pages/RunSummary/index.tsx index 7b455663964..0619552be5b 100644 --- a/app/src/pages/RunSummary/index.tsx +++ b/app/src/pages/RunSummary/index.tsx @@ -59,12 +59,11 @@ import { } from '../../redux/analytics' import { getLocalRobot } from '../../redux/discovery' import { RunFailedModal } from '../../organisms/OnDeviceDisplay/RunningProtocol' -import { formatTimeWithUtcLabel } from '../../resources/runs/utils' +import { formatTimeWithUtcLabel, useNotifyRunQuery } from '../../resources/runs' import { handleTipsAttachedModal } from '../../organisms/DropTipWizard/TipsAttachedModal' import { getPipettesWithTipAttached } from '../../organisms/DropTipWizard/getPipettesWithTipAttached' import { getPipetteModelSpecs, FLEX_ROBOT_TYPE } from '@opentrons/shared-data' import { useMostRecentRunId } from '../../organisms/ProtocolUpload/hooks/useMostRecentRunId' -import { useNotifyRunQuery } from '../../resources/runs/useNotifyRunQuery' import type { OnDeviceRouteParams } from '../../App/types' import type { PipetteModelSpecs } from '@opentrons/shared-data' diff --git a/app/src/pages/RunningProtocol/__tests__/RunningProtocol.test.tsx b/app/src/pages/RunningProtocol/__tests__/RunningProtocol.test.tsx index be0b16f591b..32f87a8047c 100644 --- a/app/src/pages/RunningProtocol/__tests__/RunningProtocol.test.tsx +++ b/app/src/pages/RunningProtocol/__tests__/RunningProtocol.test.tsx @@ -32,8 +32,10 @@ import { useTrackProtocolRunEvent } from '../../../organisms/Devices/hooks' import { useMostRecentCompletedAnalysis } from '../../../organisms/LabwarePositionCheck/useMostRecentCompletedAnalysis' import { OpenDoorAlertModal } from '../../../organisms/OpenDoorAlertModal' import { RunningProtocol } from '..' -import { useNotifyLastRunCommandKey } from '../../../resources/runs/useNotifyLastRunCommandKey' -import { useNotifyRunQuery } from '../../../resources/runs/useNotifyRunQuery' +import { + useNotifyLastRunCommandKey, + useNotifyRunQuery, +} from '../../../resources/runs' import type { UseQueryResult } from 'react-query' import type { ProtocolAnalyses } from '@opentrons/api-client' @@ -50,9 +52,7 @@ vi.mock('../../../organisms/OnDeviceDisplay/RunningProtocol') vi.mock('../../../redux/discovery') vi.mock('../../../organisms/OnDeviceDisplay/RunningProtocol/CancelingRunModal') vi.mock('../../../organisms/OpenDoorAlertModal') -vi.mock('../../../resources/runs/useNotifyLastRunCommandKey') -vi.mock('../../../resources/runs/useNotifyRunQuery') - +vi.mock('../../../resources/runs') const RUN_ID = 'run_id' const ROBOT_NAME = 'otie' const PROTOCOL_ID = 'protocol_id' diff --git a/app/src/pages/RunningProtocol/index.tsx b/app/src/pages/RunningProtocol/index.tsx index a702b7bf881..2fc56806679 100644 --- a/app/src/pages/RunningProtocol/index.tsx +++ b/app/src/pages/RunningProtocol/index.tsx @@ -29,7 +29,10 @@ import { import { StepMeter } from '../../atoms/StepMeter' import { useMostRecentCompletedAnalysis } from '../../organisms/LabwarePositionCheck/useMostRecentCompletedAnalysis' -import { useNotifyLastRunCommandKey } from '../../resources/runs/useNotifyLastRunCommandKey' +import { + useNotifyLastRunCommandKey, + useNotifyRunQuery, +} from '../../resources/runs' import { InterventionModal } from '../../organisms/InterventionModal' import { isInterventionCommand } from '../../organisms/InterventionModal/utils' import { @@ -50,7 +53,6 @@ import { CancelingRunModal } from '../../organisms/OnDeviceDisplay/RunningProtoc import { ConfirmCancelRunModal } from '../../organisms/OnDeviceDisplay/RunningProtocol/ConfirmCancelRunModal' import { getLocalRobot } from '../../redux/discovery' import { OpenDoorAlertModal } from '../../organisms/OpenDoorAlertModal' -import { useNotifyRunQuery } from '../../resources/runs/useNotifyRunQuery' import type { OnDeviceRouteParams } from '../../App/types' diff --git a/app/src/redux/config/constants.ts b/app/src/redux/config/constants.ts index 34e8c943d8a..0f6dbba7a2d 100644 --- a/app/src/redux/config/constants.ts +++ b/app/src/redux/config/constants.ts @@ -1,6 +1,9 @@ import type { DevInternalFlag } from './types' -export const DEV_INTERNAL_FLAGS: DevInternalFlag[] = ['protocolStats'] +export const DEV_INTERNAL_FLAGS: DevInternalFlag[] = [ + 'protocolStats', + 'enableRunTimeParameters', +] // action type constants export const INITIALIZED: 'config:INITIALIZED' = 'config:INITIALIZED' diff --git a/app/src/redux/config/schema-types.ts b/app/src/redux/config/schema-types.ts index dea58c435d2..d0412a4b501 100644 --- a/app/src/redux/config/schema-types.ts +++ b/app/src/redux/config/schema-types.ts @@ -7,7 +7,7 @@ export type UpdateChannel = 'latest' | 'beta' | 'alpha' export type DiscoveryCandidates = string[] -export type DevInternalFlag = 'protocolStats' +export type DevInternalFlag = 'protocolStats' | 'enableRunTimeParameters' export type FeatureFlags = Partial> diff --git a/app/src/redux/shell/types.ts b/app/src/redux/shell/types.ts index 379d22bd892..6502f92c439 100644 --- a/app/src/redux/shell/types.ts +++ b/app/src/redux/shell/types.ts @@ -19,13 +19,17 @@ export interface Remote { } } -interface NotifyRefetchData { +export interface NotifyRefetchData { refetchUsingHTTP: boolean - statusCode: never } +export interface NotifyUnsubscribeData { + unsubscribe: boolean +} + +export type NotifyBrokerResponses = NotifyRefetchData | NotifyUnsubscribeData export type NotifyNetworkError = 'ECONNFAILED' | 'ECONNREFUSED' -export type NotifyResponseData = NotifyRefetchData | NotifyNetworkError +export type NotifyResponseData = NotifyBrokerResponses | NotifyNetworkError interface File { sha512: string diff --git a/app/src/resources/__tests__/useNotifyService.test.ts b/app/src/resources/__tests__/useNotifyService.test.ts index d1ad8951421..0b4ce2fd1b0 100644 --- a/app/src/resources/__tests__/useNotifyService.test.ts +++ b/app/src/resources/__tests__/useNotifyService.test.ts @@ -171,4 +171,21 @@ describe('useNotifyService', () => { rerender() expect(mockHTTPRefetch).toHaveBeenCalledWith('once') }) + + it('should trigger a single HTTP refetch if the unsubscribe flag was returned', () => { + vi.mocked(appShellListener).mockImplementation( + (_: any, __: any, mockCb: any) => { + mockCb({ unsubscribe: true }) + } + ) + const { rerender } = renderHook(() => + useNotifyService({ + topic: MOCK_TOPIC, + setRefetchUsingHTTP: mockHTTPRefetch, + options: MOCK_OPTIONS, + } as any) + ) + rerender() + expect(mockHTTPRefetch).toHaveBeenCalledWith('once') + }) }) diff --git a/app/src/resources/maintenance_runs/index.ts b/app/src/resources/maintenance_runs/index.ts new file mode 100644 index 00000000000..ecd7a95a94d --- /dev/null +++ b/app/src/resources/maintenance_runs/index.ts @@ -0,0 +1 @@ +export * from './useNotifyCurrentMaintenanceRun' diff --git a/app/src/resources/runs/index.ts b/app/src/resources/runs/index.ts new file mode 100644 index 00000000000..be5fabb4970 --- /dev/null +++ b/app/src/resources/runs/index.ts @@ -0,0 +1,5 @@ +export * from './hooks' +export * from './utils' +export * from './useNotifyAllRunsQuery' +export * from './useNotifyRunQuery' +export * from './useNotifyLastRunCommandKey' diff --git a/app/src/resources/runs/useNotifyRunQuery.ts b/app/src/resources/runs/useNotifyRunQuery.ts index 1da90ee7a08..d36110c37f1 100644 --- a/app/src/resources/runs/useNotifyRunQuery.ts +++ b/app/src/resources/runs/useNotifyRunQuery.ts @@ -24,7 +24,7 @@ export function useNotifyRunQuery( useNotifyService({ topic: `robot-server/runs/${runId}` as NotifyTopic, setRefetchUsingHTTP, - options: { ...options, enabled: options.enabled && runId != null }, + options: { ...options, enabled: options.enabled != null && runId != null }, }) const httpResponse = useRunQuery(runId, { diff --git a/app/src/resources/useNotifyService.ts b/app/src/resources/useNotifyService.ts index 3accf0b8082..8fcfd852575 100644 --- a/app/src/resources/useNotifyService.ts +++ b/app/src/resources/useNotifyService.ts @@ -77,7 +77,7 @@ export function useNotifyService({ properties: {}, }) } - } else if ('refetchUsingHTTP' in data) { + } else if ('refetchUsingHTTP' in data || 'unsubscribe' in data) { setRefetchUsingHTTP('once') } } diff --git a/hardware-testing/hardware_testing/drivers/asair_sensor.py b/hardware-testing/hardware_testing/drivers/asair_sensor.py index f1d694cf105..350741ebc79 100644 --- a/hardware-testing/hardware_testing/drivers/asair_sensor.py +++ b/hardware-testing/hardware_testing/drivers/asair_sensor.py @@ -92,8 +92,9 @@ def BuildAsairSensor(simulate: bool, autosearch: bool = True) -> AsairSensorBase ui.print_info(f"Trying to connect to env sensor on port {port}") sensor = AsairSensor.connect(port) ser_id = sensor.get_serial() - ui.print_info(f"Found env sensor {ser_id} on port {port}") - return sensor + if len(ser_id) != 0: + ui.print_info(f"Found env sensor {ser_id} on port {port}") + return sensor except: # noqa: E722 pass use_sim = ui.get_user_answer("No env sensor found, use simulator?") diff --git a/hardware-testing/hardware_testing/gravimetric/tips.py b/hardware-testing/hardware_testing/gravimetric/tips.py index 520a959cd77..8edf66a5797 100644 --- a/hardware-testing/hardware_testing/gravimetric/tips.py +++ b/hardware-testing/hardware_testing/gravimetric/tips.py @@ -1,7 +1,12 @@ """Multi-Channel Tips.""" from typing import List, Dict -from opentrons.protocol_api import ProtocolContext, Well, Labware, InstrumentContext +from opentrons.protocol_api import ( + ProtocolContext, + Well, + Labware, + InstrumentContext, +) # Rows by Channel: # - Rear Racks (slot-row=C) @@ -100,34 +105,47 @@ def _get_racks(ctx: ProtocolContext) -> Dict[int, Labware]: } -def _unused_tips_for_racks(racks: List[Labware]) -> List[Well]: +def _unused_tips_for_racks( + ctx: ProtocolContext, pipette_mount: str, racks: List[Labware] +) -> List[Well]: wells: List[Well] = [] rows = "ABCDEFGH" for rack in racks: for col in range(1, 13): for row in rows: wellname = f"{row}{col}" - next_well = rack.next_tip(1, rack[wellname]) + next_well = rack.next_tip( + 1, + rack[wellname], + ) if next_well is not None and wellname == next_well.well_name: wells.append(rack[wellname]) return wells -def get_unused_tips(ctx: ProtocolContext, tip_volume: int) -> List[Well]: +def get_unused_tips( + ctx: ProtocolContext, tip_volume: int, pipette_mount: str +) -> List[Well]: """Use the labware's tip tracker to get a list of all unused tips for a given tip volume.""" racks = [ r for r in _get_racks(ctx).values() if r.wells()[0].max_volume == tip_volume ] - return _unused_tips_for_racks(racks) + return _unused_tips_for_racks(ctx, pipette_mount, racks) -def get_tips_for_single(ctx: ProtocolContext, tip_volume: int) -> List[Well]: +def get_tips_for_single( + ctx: ProtocolContext, tip_volume: int, pipette_mount: str +) -> List[Well]: """Get tips for single channel.""" - return get_unused_tips(ctx, tip_volume) + return get_unused_tips(ctx, tip_volume, pipette_mount) def get_tips_for_individual_channel_on_multi( - ctx: ProtocolContext, channel: int, tip_volume: int, pipette_volume: int + ctx: ProtocolContext, + channel: int, + tip_volume: int, + pipette_volume: int, + pipette_mount: str, ) -> List[Well]: """Get tips for a multi's channel.""" print(f"getting {tip_volume} tips for channel {channel}") @@ -140,7 +158,7 @@ def get_tips_for_individual_channel_on_multi( specific_racks: List[Labware] = [] for slot in slots: specific_racks.append(all_racks[slot]) - unused_tips = _unused_tips_for_racks(specific_racks) + unused_tips = _unused_tips_for_racks(ctx, pipette_mount, specific_racks) tips = [ tip for tip in unused_tips @@ -171,14 +189,14 @@ def get_tips( ) -> Dict[int, List[Well]]: """Get tips.""" if pipette.channels == 1: - return {0: get_tips_for_single(ctx, tip_volume)} + return {0: get_tips_for_single(ctx, tip_volume, pipette.mount)} elif pipette.channels == 8: if all_channels: return {0: get_tips_for_all_channels_on_multi(ctx, tip_volume)} else: return { channel: get_tips_for_individual_channel_on_multi( - ctx, channel, tip_volume, int(pipette.max_volume) + ctx, channel, tip_volume, int(pipette.max_volume), pipette.mount ) for channel in range(pipette.channels) } diff --git a/hardware-testing/hardware_testing/production_qc/pipette_assembly_qc_ot3/__main__.py b/hardware-testing/hardware_testing/production_qc/pipette_assembly_qc_ot3/__main__.py index 207791f58ab..80d3993e6c5 100644 --- a/hardware-testing/hardware_testing/production_qc/pipette_assembly_qc_ot3/__main__.py +++ b/hardware-testing/hardware_testing/production_qc/pipette_assembly_qc_ot3/__main__.py @@ -472,7 +472,6 @@ def _connect_to_fixture(test_config: TestConfig) -> PressureFixtureBase: fixture = connect_to_fixture( test_config.simulate or test_config.skip_fixture, side=test_config.fixture_side ) - fixture.connect() return fixture diff --git a/hardware-testing/hardware_testing/protocols/installation_qualification/flex_iq_p1000_multi_200ul.py b/hardware-testing/hardware_testing/protocols/installation_qualification/flex_iq_p1000_multi_200ul.py index 2e5f13c11f4..7b75ab7a590 100644 --- a/hardware-testing/hardware_testing/protocols/installation_qualification/flex_iq_p1000_multi_200ul.py +++ b/hardware-testing/hardware_testing/protocols/installation_qualification/flex_iq_p1000_multi_200ul.py @@ -287,7 +287,11 @@ def _transfer( # transfer if not same_tip: pipette.configure_for_volume(volume) - pipette.pick_up_tip(tips.next_tip(pipette.channels)) + pipette.pick_up_tip( + tips.next_tip( + pipette.channels, + ) + ) if pipette.current_volume > 0: pipette.dispense(pipette.current_volume, reservoir[source].top()) pipette.aspirate(volume, aspirate_pos) diff --git a/hardware-testing/hardware_testing/protocols/installation_qualification/flex_iq_p50_multi_1ul.py b/hardware-testing/hardware_testing/protocols/installation_qualification/flex_iq_p50_multi_1ul.py index 43fd03d1f6c..5953ef76c0f 100644 --- a/hardware-testing/hardware_testing/protocols/installation_qualification/flex_iq_p50_multi_1ul.py +++ b/hardware-testing/hardware_testing/protocols/installation_qualification/flex_iq_p50_multi_1ul.py @@ -296,7 +296,11 @@ def _transfer( # transfer if not same_tip: pipette.configure_for_volume(volume) - pipette.pick_up_tip(tips.next_tip(pipette.channels)) + pipette.pick_up_tip( + tips.next_tip( + pipette.channels, + ) + ) if pipette.current_volume > 0: pipette.dispense(pipette.current_volume, reservoir[source].top()) pipette.aspirate(volume, aspirate_pos) diff --git a/hardware-testing/hardware_testing/protocols/installation_qualification/flex_iq_p50_single_1ul.py b/hardware-testing/hardware_testing/protocols/installation_qualification/flex_iq_p50_single_1ul.py index 8160d43cb9c..17c76019dd5 100644 --- a/hardware-testing/hardware_testing/protocols/installation_qualification/flex_iq_p50_single_1ul.py +++ b/hardware-testing/hardware_testing/protocols/installation_qualification/flex_iq_p50_single_1ul.py @@ -283,7 +283,11 @@ def _transfer( # transfer if not same_tip: pipette.configure_for_volume(volume) - pipette.pick_up_tip(tips.next_tip(pipette.channels)) + pipette.pick_up_tip( + tips.next_tip( + pipette.channels, + ) + ) if pipette.current_volume > 0: pipette.dispense(pipette.current_volume, reservoir[source].top()) pipette.aspirate(volume, aspirate_pos) diff --git a/hardware/opentrons_hardware/scripts/lld_data_script.py b/hardware/opentrons_hardware/scripts/lld_data_script.py new file mode 100644 index 00000000000..3baa2e4049e --- /dev/null +++ b/hardware/opentrons_hardware/scripts/lld_data_script.py @@ -0,0 +1,441 @@ +"""Script that can process previous real world data to test lld processes.""" +import csv +import os +import argparse +from typing import List, Optional, Tuple, Any, Dict +import matplotlib.pyplot as plot +import numpy +from abc import ABC, abstractmethod + +impossible_pressure = 9001.0 +accepted_error = 0.1 + + +class LLDAlgoABC(ABC): + """An instance of an lld algorithm.""" + + @staticmethod + @abstractmethod + def name() -> str: + """Name of this algorithm.""" + ... + + @abstractmethod + def tick(self, pressure: float) -> Tuple[bool, float]: + """Simulate firmware motor interrupt tick.""" + ... + + @abstractmethod + def reset(self) -> None: + """Reset simulator between runs.""" + ... + + +class LLDPresThresh(LLDAlgoABC): + """present day threshold based.""" + + threshold: float + + def __init__(self, thresh: float = -150) -> None: + """Init.""" + self.threshold = thresh + + @staticmethod + def name() -> str: + """Name of this algorithm.""" + return "{:<30}".format("simple threshold") + + def tick(self, pressure: float) -> Tuple[bool, float]: + """Simulate firmware motor interrupt tick.""" + return (pressure < self.threshold, pressure) + + def reset(self) -> None: + """Reset simulator between runs.""" + pass + + +class LLDSMAT(LLDAlgoABC): + """Simple moving average threshold.""" + + samples_n_smat: int + running_samples_smat: List[float] + threshold_smat: float + + def __init__(self, samples: int = 10, thresh: float = -15) -> None: + """Init.""" + self.samples_n_smat = samples + self.threshold_smat = thresh + self.reset() + + @staticmethod + def name() -> str: + """Name of this algorithm.""" + return "{:<30}".format("simple moving avg thresh") + + def reset(self) -> None: + """Reset simulator between runs.""" + self.running_samples_smat = [impossible_pressure] * self.samples_n_smat + + def tick(self, pressure: float) -> Tuple[bool, float]: + """Simulate firmware motor interrupt tick.""" + try: + next_ind = self.running_samples_smat.index(impossible_pressure) + # if no exception we're still filling the minimum samples + self.running_samples_smat[next_ind] = pressure + return (False, impossible_pressure) + except ValueError: # the array has been filled + pass + # left shift old samples + for i in range(self.samples_n_smat - 1): + self.running_samples_smat[i] = self.running_samples_smat[i + 1] + self.running_samples_smat[self.samples_n_smat - 1] = pressure + new_running_avg = sum(self.running_samples_smat) / self.samples_n_smat + return ( + new_running_avg < self.threshold_smat, + new_running_avg, + ) + + +class LLDSMAD(LLDAlgoABC): + """Simple moving average derivative.""" + + samples_n_smad: int + running_samples_smad: List[float] + derivative_threshold_smad: float + + def __init__(self, samples: int = 10, thresh: float = -2.5) -> None: + """Init.""" + self.samples_n_smad = samples + self.derivative_threshold_smad = thresh + self.reset() + + @staticmethod + def name() -> str: + """Name of this algorithm.""" + return "{:<30}".format("simple moving avg der") + + def reset(self) -> None: + """Reset simulator between runs.""" + self.running_samples_smad = [impossible_pressure] * self.samples_n_smad + + def tick(self, pressure: float) -> Tuple[bool, float]: + """Simulate firmware motor interrupt tick.""" + try: + next_ind = self.running_samples_smad.index(impossible_pressure) + # if no exception we're still filling the minimum samples + self.running_samples_smad[next_ind] = pressure + return (False, impossible_pressure) + except ValueError: # the array has been filled + pass + # store old running average + prev_running_avg = sum(self.running_samples_smad) / self.samples_n_smad + # left shift old samples + for i in range(self.samples_n_smad - 1): + self.running_samples_smad[i] = self.running_samples_smad[i + 1] + self.running_samples_smad[self.samples_n_smad - 1] = pressure + new_running_avg = sum(self.running_samples_smad) / self.samples_n_smad + return ( + (new_running_avg - prev_running_avg) < self.derivative_threshold_smad, + new_running_avg, + ) + + +class LLDWMAD(LLDAlgoABC): + """Weighted moving average derivative.""" + + samples_n_wmad: int + weights_wmad: numpy.ndarray[Any, numpy.dtype[numpy.float32]] = numpy.array( + [0.19, 0.17, 0.15, 0.13, 0.11, 0.09, 0.07, 0.05, 0.03, 0.01] + ) + running_samples_wmad: numpy.ndarray[Any, numpy.dtype[numpy.float32]] + derivative_threshold_wmad: float + + def __init__(self, samples: int = 10, thresh: float = -4) -> None: + """Init.""" + self.samples_n_wmad = samples + self.derivative_threshold_wmad = thresh + self.reset() + + @staticmethod + def name() -> str: + """Name of this algorithm.""" + return "{:<30}".format("weighted moving avg der") + + def reset(self) -> None: + """Reset simulator between runs.""" + assert numpy.sum(self.weights_wmad) == 1 + self.running_samples_wmad = numpy.full(self.samples_n_wmad, impossible_pressure) + + def tick(self, pressure: float) -> Tuple[bool, float]: + """Simulate firmware motor interrupt tick.""" + if numpy.isin(impossible_pressure, self.running_samples_wmad): + next_ind = numpy.where(self.running_samples_wmad == impossible_pressure)[0][ + 0 + ] + # if no exception we're still filling the minimum samples + self.running_samples_wmad[next_ind] = pressure + return (False, impossible_pressure) + # store old running average + prev_running_avg = numpy.sum( + numpy.multiply(self.running_samples_wmad, self.weights_wmad) + ) + # left shift old samples + for i in range(self.samples_n_wmad - 1): + self.running_samples_wmad[i] = self.running_samples_wmad[i + 1] + self.running_samples_wmad[self.samples_n_wmad - 1] = pressure + new_running_avg = numpy.sum( + numpy.multiply(self.running_samples_wmad, self.weights_wmad) + ) + return ( + (new_running_avg - prev_running_avg) < self.derivative_threshold_wmad, + new_running_avg, + ) + + +class LLDEMAD(LLDAlgoABC): + """Exponential moving average derivative.""" + + current_average_emad: float = impossible_pressure + smoothing_factor: float + derivative_threshold_emad: float + + def __init__(self, s_factor: float = 0.1, thresh: float = -2.5) -> None: + """Init.""" + self.smoothing_factor = s_factor + self.derivative_threshold_emad = thresh + self.reset() + + @staticmethod + def name() -> str: + """Name of this algorithm.""" + return "{:<30}".format("exponential moving avg der") + + def reset(self) -> None: + """Reset simulator between runs.""" + self.current_average_emad = impossible_pressure + + def tick(self, pressure: float) -> Tuple[bool, float]: + """Simulate firmware motor interrupt tick.""" + if self.current_average_emad == impossible_pressure: + self.current_average_emad = pressure + return (False, impossible_pressure) + else: + new_average = (pressure * self.smoothing_factor) + ( + self.current_average_emad * (1 - self.smoothing_factor) + ) + derivative = new_average - self.current_average_emad + self.current_average_emad = new_average + return ( + derivative < self.derivative_threshold_emad, + self.current_average_emad, + ) + + +def _running_avg( + time: List[float], + pressure: List[float], + z_travel: List[float], + p_travel: List[float], + no_plot: bool, + algorithm: LLDAlgoABC, + plot_name: str, +) -> Optional[Tuple[float, float, float]]: + algorithm.reset() + average = float(0) + running_time = [] + running_derivative = [] + running_avg = [] + return_val = None + for i in range(1, len(time)): + prev_avg = average + found, average = algorithm.tick(float(pressure[i])) + if found: + # if average < running_avg_threshold: + # print(f"found z height = {z_travel[i]}") + # print(f"at time = {time[i]}") + return_val = time[i], z_travel[i], p_travel[i] + if no_plot: + # once we find it we don't need to keep going + break + if average != impossible_pressure and prev_avg != impossible_pressure: + running_avg_derivative = average - prev_avg + running_time.append(time[i]) + running_derivative.append(running_avg_derivative) + running_avg.append(average) + + time_array: numpy.ndarray[Any, numpy.dtype[numpy.float32]] = numpy.array( + running_time + ) + derivative_array: numpy.ndarray[Any, numpy.dtype[numpy.float32]] = numpy.array( + running_derivative + ) + avg_array: numpy.ndarray[Any, numpy.dtype[numpy.float32]] = numpy.array(running_avg) + + if not no_plot: + plot.figure(plot_name) + avg_ax = plot.subplot(211) + avg_ax.set_title("Running Average") + plot.plot(time_array, avg_array) + der_ax = plot.subplot(212) + der_ax.set_title("Derivative") + plot.plot(time_array, derivative_array) + mng = plot.get_current_fig_manager() + if mng is not None: + mng.resize(*mng.window.maxsize()) # type: ignore[attr-defined] + plot.show() + + return return_val + + +def run( + args: argparse.Namespace, + algorithm: LLDAlgoABC, +) -> List[Tuple[float, List[float], str, str]]: + """Run the test with a given algorithm on all the data.""" + path = args.filepath + "/" + report_files = [ + file for file in os.listdir(args.filepath) if "final_report" in file + ] + final_results: List[Tuple[float, List[float], str, str]] = [] + for report_file in report_files: + with open(path + report_file, "r") as file: + reader = csv.reader(file) + reader_list = list(reader) + + number_of_trials = int(reader_list[34][2]) + + expected_height = reader_list[44][6] + # have a time list for each trial so the list lengths can all be equal + results: List[float] = [] + for trial in range(number_of_trials): + + time = [] + pressure = [] + z_travel = [] + p_travel = [] + for row in range((59 + number_of_trials), len(reader_list)): + current_time = reader_list[row][0] + current_pressure = reader_list[row][3 * trial + 2] + current_z_pos = reader_list[row][3 * trial + 3] + current_p_pos = reader_list[row][3 * trial + 4] + + if any( + [ + data == "" + for data in [current_pressure, current_z_pos, current_p_pos] + ] + ): + break + + time.append(float(current_time)) + pressure.append(float(current_pressure)) + z_travel.append(float(current_z_pos)) + p_travel.append(float(current_p_pos)) + + threshold_data = _running_avg( + time, + pressure, + z_travel, + p_travel, + args.no_plot, + algorithm, + f"{algorithm.name()} trial: {trial+1}", + ) + if threshold_data: + # threshold_time = threshold_data[0] + threshold_z_pos = threshold_data[1] + # threshold_p_pos = threshold_data[2] + # print( + # f"Threshold found at:\n\ttime: {threshold_time}\n\tz distance: {threshold_z_pos}\n\tp distance: {threshold_p_pos}" + # ) + results.append(float(threshold_z_pos)) + else: + print("No threshold found") + print( + f"{algorithm.name()}, expected {expected_height} max {max(results)} min{min(results)}, avg {sum(results)/len(results)}" + ) + final_results.append( + (float(expected_height), results, f"{algorithm.name()}", f"{report_file}") + ) + return final_results + + +def _check_for_failure(expected_height: float, results: List[float]) -> bool: + for result in results: + if abs(expected_height - result) > accepted_error: + return True + return False + + +def _score( + algorithms: List[LLDAlgoABC], analysis: List[Tuple[float, List[float], str, str]] +) -> Dict[str, int]: + algorithm_score: Dict[str, int] = {algo.name(): 0 for algo in algorithms} + a_score = len(analysis) + for a in analysis: + algorithm_score[a[2]] += a_score + a_score -= 2 + return dict(sorted(algorithm_score.items(), key=lambda item: item[1], reverse=True)) + + +def main() -> None: + """Main function.""" + # data starts at row 59 + number of trials + + parser = argparse.ArgumentParser() + parser.add_argument( + "--filepath", + type=str, + help="path to the input file", + default=None, + ) + parser.add_argument("--no-plot", action="store_true") + args = parser.parse_args() + + algorithms: List[LLDAlgoABC] = [ + LLDPresThresh(), + LLDSMAD(), + LLDWMAD(), + LLDEMAD(), + LLDSMAT(), + ] + analysis: List[Tuple[float, List[float], str, str]] = [] + for algorithm in algorithms: + algorithm_results = run(args, algorithm) + analysis.extend(algorithm_results) + print("\n\n") + for result in analysis: + res_string = ( + "FAILURE" if _check_for_failure(result[0], result[1]) else "success" + ) + print(f"Algorithm {result[2]} {res_string}") + + accuracy = sorted( + analysis, key=lambda acc: abs((sum(acc[1]) / len(acc[1])) - acc[0]) + ) + precision = sorted(analysis, key=lambda per: (max(per[1]) - min(per[1]))) + + accuracy_score: Dict[str, int] = _score(algorithms, accuracy) + precision_score: Dict[str, int] = _score(algorithms, precision) + algorithm_score: Dict[str, int] = {algo.name(): 0 for algo in algorithms} + + print("Accuracy Scores") + for a_name in accuracy_score.keys(): + print(f"{a_name} {accuracy_score[a_name]}") + + print("Precision Scores") + for a_name in precision_score.keys(): + print(f"{a_name} {precision_score[a_name]}") + # add the two scores together for final score so we can sort before printing + algorithm_score[a_name] = precision_score[a_name] + accuracy_score[a_name] + + algorithm_score = dict( + sorted(algorithm_score.items(), key=lambda item: item[1], reverse=True) + ) + print("Total Scores") + for a_name in algorithm_score.keys(): + print(f"{a_name} {algorithm_score[a_name]}") + + +if __name__ == "__main__": + main() diff --git a/labware-library/cypress/integration/labware-creator/customTubeRack.spec.js b/labware-library/cypress/integration/labware-creator/customTubeRack.spec.js index 11abf3aacd9..94a3155257f 100644 --- a/labware-library/cypress/integration/labware-creator/customTubeRack.spec.js +++ b/labware-library/cypress/integration/labware-creator/customTubeRack.spec.js @@ -24,9 +24,7 @@ context('Tubes and Rack', () => { .children() .first() .trigger('mousedown') - cy.get('*[class^="Dropdown__option_label"]') - .contains('Tubes + Tube Rack') - .click() + cy.get('*[class^="_option_label"]').contains('Tubes + Tube Rack').click() // TODO(IL, 2021-05-15): give Dropdown component semantic selectors for E2E cy.get('label') @@ -34,7 +32,7 @@ context('Tubes and Rack', () => { .children() .first() .trigger('mousedown') - cy.get('*[class^="Dropdown__option_label"]') + cy.get('*[class^="_option_label"]') .contains('Non-Opentrons tube rack') .click() @@ -198,7 +196,7 @@ context('Tubes and Rack', () => { .children() .first() .trigger('mousedown') - cy.get('*[class^="Dropdown__option_label"]') + cy.get('*[class^="_option_label"]') .contains(/P20.*Single-Channel.*GEN2/) .click() cy.contains('Test Pipette is a required field').should('not.exist') diff --git a/labware-library/cypress/integration/labware-creator/fileImport.spec.js b/labware-library/cypress/integration/labware-creator/fileImport.spec.js index 4ad4bf03af9..a7294a979c1 100644 --- a/labware-library/cypress/integration/labware-creator/fileImport.spec.js +++ b/labware-library/cypress/integration/labware-creator/fileImport.spec.js @@ -14,7 +14,7 @@ context('File Import', () => { it('drags in a file', () => { cy.fixture(importedLabwareFile, 'utf8').then(fileJson => { const fileContent = JSON.stringify(fileJson) - cy.get('[class*="_file_drop__"]').upload( + cy.get('[class*="file_drop"]').first().upload( { fileContent, fileName: importedLabwareFile, @@ -110,7 +110,7 @@ context('File Import', () => { .children() .first() .trigger('mousedown') - cy.get('*[class^="Dropdown__option_label"]') + cy.get('*[class^="_option_label"]') .contains(/P10.*Single-Channel.*GEN1/) .click() diff --git a/labware-library/cypress/integration/labware-creator/reservoir.spec.js b/labware-library/cypress/integration/labware-creator/reservoir.spec.js index 5aad9c7a81f..994b1f65017 100644 --- a/labware-library/cypress/integration/labware-creator/reservoir.spec.js +++ b/labware-library/cypress/integration/labware-creator/reservoir.spec.js @@ -16,7 +16,7 @@ context('Reservoirs', () => { .children() .first() .trigger('mousedown') - cy.get('*[class^="Dropdown__option_label"]').contains('Reservoir').click() + cy.get('*[class^="_option_label"]').contains('Reservoir').click() cy.contains('Reservoir').click({ force: true }) cy.contains('start creating labware').click({ force: true }) }) @@ -233,7 +233,7 @@ context('Reservoirs', () => { .children() .first() .trigger('mousedown') - cy.get('*[class^="Dropdown__option_label"]') + cy.get('*[class^="_option_label"]') .contains(/P10.*Single-Channel.*GEN1/) .click() cy.contains('Test Pipette is a required field').should('not.exist') diff --git a/labware-library/cypress/integration/labware-creator/tipRack.spec.js b/labware-library/cypress/integration/labware-creator/tipRack.spec.js index c824ab51a31..f45fe1e9473 100644 --- a/labware-library/cypress/integration/labware-creator/tipRack.spec.js +++ b/labware-library/cypress/integration/labware-creator/tipRack.spec.js @@ -16,7 +16,7 @@ describe('Create a Tip Rack', () => { .children() .first() .trigger('mousedown') - cy.get('*[class^="Dropdown__option_label"]').contains('Tip Rack').click() + cy.get('*[class^="_option_label"]').contains('Tip Rack').click() cy.get('button').contains('start creating labware').click({ force: true }) }) @@ -266,7 +266,7 @@ describe('Create a Tip Rack', () => { cy.get('input[name="pipetteName"]') .invoke('attr', 'value', 'p20_single_gen2') .should('have.attr', 'value', 'p20_single_gen2') - cy.get('*[class^="Dropdown__option"]') + cy.get('*[class^="_option_label"]') .contains(/P20.*Single-Channel.*GEN2/) .click() cy.get('#DefinitionTest a').contains('tip rack test guide').click() diff --git a/labware-library/cypress/integration/labware-creator/tubesBlock.spec.js b/labware-library/cypress/integration/labware-creator/tubesBlock.spec.js index 172042c7a43..40c9660ff61 100644 --- a/labware-library/cypress/integration/labware-creator/tubesBlock.spec.js +++ b/labware-library/cypress/integration/labware-creator/tubesBlock.spec.js @@ -16,7 +16,7 @@ context('Tubes and Block', () => { .children() .first() .trigger('mousedown') - cy.get('*[class^="Dropdown__option_label"]') + cy.get('*[class^="_option_label"]') .contains('Tubes / Plates + Opentrons Aluminum Block') .click() @@ -26,7 +26,7 @@ context('Tubes and Block', () => { .children() .first() .trigger('mousedown') - cy.get('*[class^="Dropdown__option_label"]').contains('96 well').click() + cy.get('*[class^="_option_label"]').contains('96 well').click() // TODO(IL, 2021-05-15): give Dropdown component semantic selectors for E2E cy.get('label') @@ -34,7 +34,7 @@ context('Tubes and Block', () => { .children() .first() .trigger('mousedown') - cy.get('*[class^="Dropdown__option_label"]') + cy.get('*[class^="_option_label"]') .contains(/^Tubes$/) .click() @@ -179,7 +179,7 @@ context('Tubes and Block', () => { .children() .first() .trigger('mousedown') - cy.get('*[class^="Dropdown__option_label"]') + cy.get('*[class^="_option_label"]') .contains(/P10.*Single-Channel.*GEN1/) .click() cy.contains('Test Pipette is a required field').should('not.exist') @@ -205,7 +205,7 @@ context('Tubes and Block', () => { .children() .first() .trigger('mousedown') - cy.get('*[class^="Dropdown__option_label"]') + cy.get('*[class^="_option_label"]') .contains('Tubes / Plates + Opentrons Aluminum Block') .click() @@ -215,7 +215,7 @@ context('Tubes and Block', () => { .children() .first() .trigger('mousedown') - cy.get('*[class^="Dropdown__option_label"]').contains('96 well').click() + cy.get('*[class^="_option_label"]').contains('96 well').click() // TODO(IL, 2021-05-15): give Dropdown component semantic selectors for E2E cy.get('label') @@ -223,9 +223,7 @@ context('Tubes and Block', () => { .children() .first() .trigger('mousedown') - cy.get('*[class^="Dropdown__option_label"]') - .contains('PCR Tube Strip') - .click() + cy.get('*[class^="_option_label"]').contains('PCR Tube Strip').click() cy.contains('start creating labware').click({ force: true }) }) @@ -368,7 +366,7 @@ context('Tubes and Block', () => { .children() .first() .trigger('mousedown') - cy.get('*[class^="Dropdown__option_label"]') + cy.get('*[class^="_option_label"]') .contains(/P10.*Single-Channel.*GEN1/) .click() cy.contains('Test Pipette is a required field').should('not.exist') @@ -394,7 +392,7 @@ context('Tubes and Block', () => { .children() .first() .trigger('mousedown') - cy.get('*[class^="Dropdown__option_label"]') + cy.get('*[class^="_option_label"]') .contains('Tubes / Plates + Opentrons Aluminum Block') .click() @@ -404,7 +402,7 @@ context('Tubes and Block', () => { .children() .first() .trigger('mousedown') - cy.get('*[class^="Dropdown__option_label"]').contains('96 well').click() + cy.get('*[class^="_option_label"]').contains('96 well').click() // TODO(IL, 2021-05-15): give Dropdown component semantic selectors for E2E cy.get('label') @@ -412,9 +410,7 @@ context('Tubes and Block', () => { .children() .first() .trigger('mousedown') - cy.get('*[class^="Dropdown__option_label"]') - .contains('PCR Plate') - .click() + cy.get('*[class^="_option_label"]').contains('PCR Plate').click() cy.contains('start creating labware').click({ force: true }) }) @@ -557,7 +553,7 @@ context('Tubes and Block', () => { .children() .first() .trigger('mousedown') - cy.get('*[class^="Dropdown__option_label"]') + cy.get('*[class^="_option_label"]') .contains(/P10.*Single-Channel.*GEN1/) .click() cy.contains('Test Pipette is a required field').should('not.exist') @@ -585,7 +581,7 @@ context('Tubes and Block', () => { .children() .first() .trigger('mousedown') - cy.get('*[class^="Dropdown__option_label"]') + cy.get('*[class^="_option_label"]') .contains('Tubes / Plates + Opentrons Aluminum Block') .click() @@ -595,7 +591,7 @@ context('Tubes and Block', () => { .children() .first() .trigger('mousedown') - cy.get('*[class^="Dropdown__option_label"]').contains('24 well').click() + cy.get('*[class^="_option_label"]').contains('24 well').click() cy.get('label') .contains('What labware is on top of your aluminum block?') @@ -742,7 +738,7 @@ context('Tubes and Block', () => { .children() .first() .trigger('mousedown') - cy.get('*[class^="Dropdown__option_label"]') + cy.get('*[class^="_option_label"]') .contains(/P10.*Single-Channel.*GEN1/) .click() cy.contains('Test Pipette is a required field').should('not.exist') diff --git a/labware-library/cypress/integration/labware-creator/tubesRack.spec.js b/labware-library/cypress/integration/labware-creator/tubesRack.spec.js index 49d51185c24..afa5b50a5a6 100644 --- a/labware-library/cypress/integration/labware-creator/tubesRack.spec.js +++ b/labware-library/cypress/integration/labware-creator/tubesRack.spec.js @@ -14,9 +14,7 @@ context('Tubes and Rack', () => { .children() .first() .trigger('mousedown') - cy.get('*[class^="Dropdown__option_label"]') - .contains('Tubes + Tube Rack') - .click() + cy.get('*[class^="_option_label"]').contains('Tubes + Tube Rack').click() // TODO(IL, 2021-05-15): give Dropdown component semantic selectors for E2E cy.get('label') @@ -24,7 +22,7 @@ context('Tubes and Rack', () => { .children() .first() .trigger('mousedown') - cy.get('*[class^="Dropdown__option_label"]').contains('6 tubes').click() + cy.get('*[class^="_option_label"]').contains('6 tubes').click() cy.contains('start creating labware').click({ force: true }) }) @@ -162,7 +160,7 @@ context('Tubes and Rack', () => { .children() .first() .trigger('mousedown') - cy.get('*[class^="Dropdown__option_label"]') + cy.get('*[class^="_option_label"]') .contains(/P10.*Single-Channel.*GEN1/) .click() cy.contains('Test Pipette is a required field').should('not.exist') @@ -188,9 +186,7 @@ context('Tubes and Rack', () => { .children() .first() .trigger('mousedown') - cy.get('*[class^="Dropdown__option_label"]') - .contains('Tubes + Tube Rack') - .click() + cy.get('*[class^="_option_label"]').contains('Tubes + Tube Rack').click() // TODO(IL, 2021-05-15): give Dropdown component semantic selectors for E2E cy.get('label') @@ -198,7 +194,7 @@ context('Tubes and Rack', () => { .children() .first() .trigger('mousedown') - cy.get('*[class^="Dropdown__option_label"]').contains('15 tubes').click() + cy.get('*[class^="_option_label"]').contains('15 tubes').click() cy.contains('start creating labware').click({ force: true }) }) @@ -338,7 +334,7 @@ context('Tubes and Rack', () => { .children() .first() .trigger('mousedown') - cy.get('*[class^="Dropdown__option_label"]') + cy.get('*[class^="_option_label"]') .contains(/P10.*Single-Channel.*GEN1/) .click() cy.contains('Test Pipette is a required field').should('not.exist') @@ -364,9 +360,7 @@ context('Tubes and Rack', () => { .children() .first() .trigger('mousedown') - cy.get('*[class^="Dropdown__option_label"]') - .contains('Tubes + Tube Rack') - .click() + cy.get('*[class^="_option_label"]').contains('Tubes + Tube Rack').click() // TODO(IL, 2021-05-15): give Dropdown component semantic selectors for E2E cy.get('label') @@ -374,7 +368,7 @@ context('Tubes and Rack', () => { .children() .first() .trigger('mousedown') - cy.get('*[class^="Dropdown__option_label"]').contains('24 tubes').click() + cy.get('*[class^="_option_label"]').contains('24 tubes').click() cy.contains('start creating labware').click({ force: true }) }) @@ -514,7 +508,7 @@ context('Tubes and Rack', () => { .children() .first() .trigger('mousedown') - cy.get('*[class^="Dropdown__option_label"]') + cy.get('*[class^="_option_label"]') .contains(/P10.*Single-Channel.*GEN1/) .click() cy.contains('Test Pipette is a required field').should('not.exist') diff --git a/labware-library/cypress/integration/labware-creator/wellPlate.spec.js b/labware-library/cypress/integration/labware-creator/wellPlate.spec.js index 2ab735e7683..becd332792b 100644 --- a/labware-library/cypress/integration/labware-creator/wellPlate.spec.js +++ b/labware-library/cypress/integration/labware-creator/wellPlate.spec.js @@ -21,9 +21,7 @@ context('Well Plates', () => { .children() .first() .trigger('mousedown') - cy.get('*[class^="Dropdown__option_label"]') - .contains('Well Plate') - .click() + cy.get('*[class^="_option_label"]').contains('Well Plate').click() cy.get('button').contains('start creating labware').click({ force: true }) }) @@ -252,7 +250,7 @@ context('Well Plates', () => { .children() .first() .trigger('mousedown') - cy.get('*[class^="Dropdown__option_label"]') + cy.get('*[class^="_option_label"]') .contains(/P10.*Single-Channel.*GEN1/) .click() cy.contains('Test Pipette is a required field').should('not.exist') diff --git a/labware-library/vite.config.ts b/labware-library/vite.config.ts index f425684be39..2db2bd80b1a 100644 --- a/labware-library/vite.config.ts +++ b/labware-library/vite.config.ts @@ -16,6 +16,8 @@ const testAliases: {} | { 'file-saver': string } = : {} export default defineConfig({ + // this makes imports relative rather than absolute + base: '', build: { // Relative to the root outDir: 'dist', diff --git a/package.json b/package.json index fb855cc9273..67e9f909547 100755 --- a/package.json +++ b/package.json @@ -13,7 +13,6 @@ "protocol-designer", "shared-data", "step-generation", - "webpack-config", "api-client", "react-api-client", "usb-bridge/node-client" diff --git a/protocol-designer/Makefile b/protocol-designer/Makefile index 3fbbb832c5e..a81f9be53cd 100644 --- a/protocol-designer/Makefile +++ b/protocol-designer/Makefile @@ -62,7 +62,7 @@ serve: all test-e2e: concurrently --no-color --kill-others --success first --names "protocol-designer-server,protocol-designer-tests" \ "$(MAKE) dev CYPRESS=1" \ - "wait-on http://localhost:8080/ && cypress run --browser chrome --headless --record false" + "wait-on http://localhost:5173/ && cypress run --browser chrome --headless --record false" .PHONY: test test: diff --git a/protocol-designer/README.md b/protocol-designer/README.md index c7bf1b3983a..ae8b91e2e52 100644 --- a/protocol-designer/README.md +++ b/protocol-designer/README.md @@ -1,10 +1,10 @@ -# Opentrons Protocol Designer Beta +# Opentrons Protocol Designer ## Overview Protocol Designer is a tool for scientists and technicians to create protocols for their [OT-2 personal pipetting robot][ot-2] without having to write any code. It provides visual feedback including liquid tracking and tip tracking to allow users to see exactly what their protocol will do at each step. The protocols are saved to Opentrons JSON Protocol files, which can be uploaded to the Opentrons Desktop App to run on a robot. -Protocol Designer Beta is optimized for [Chrome][chrome] browser. Other browsers are not fully supported. +Protocol Designer is optimized for [Chrome][chrome] browser. Other browsers are not fully supported. ## Build setup for development diff --git a/protocol-designer/cypress.json b/protocol-designer/cypress.json index 44203bdc3da..fa95795bfd6 100644 --- a/protocol-designer/cypress.json +++ b/protocol-designer/cypress.json @@ -2,5 +2,6 @@ "baseUrl": "http://localhost:5173", "video": false, "viewportWidth": 1440, - "viewportHeight": 900 + "viewportHeight": 900, + "pluginsFile": false } diff --git a/protocol-designer/cypress/integration/batchEdit.spec.js b/protocol-designer/cypress/integration/batchEdit.spec.js index 55071aae50a..300983ad9b0 100644 --- a/protocol-designer/cypress/integration/batchEdit.spec.js +++ b/protocol-designer/cypress/integration/batchEdit.spec.js @@ -1,5 +1,3 @@ -import { beforeEach, describe, it } from 'vitest' - describe('Batch Edit Transform', () => { beforeEach(() => { cy.visit('/') diff --git a/protocol-designer/cypress/integration/home.spec.js b/protocol-designer/cypress/integration/home.spec.js index 99e554e0d8f..c2f2bda9f92 100644 --- a/protocol-designer/cypress/integration/home.spec.js +++ b/protocol-designer/cypress/integration/home.spec.js @@ -1,5 +1,3 @@ -import { beforeEach, describe, it } from 'vitest' - describe('The Home Page', () => { beforeEach(() => { cy.visit('/') @@ -7,7 +5,7 @@ describe('The Home Page', () => { }) it('successfully loads', () => { - cy.title().should('equal', 'Opentrons Protocol Designer BETA') + cy.title().should('equal', 'Opentrons Protocol Designer') }) it('has the right charset', () => { diff --git a/protocol-designer/cypress/integration/migrations.spec.js b/protocol-designer/cypress/integration/migrations.spec.js index 0fad10a0a10..6bc1036477a 100644 --- a/protocol-designer/cypress/integration/migrations.spec.js +++ b/protocol-designer/cypress/integration/migrations.spec.js @@ -1,4 +1,3 @@ -import { beforeEach, describe, it } from 'vitest' import 'cypress-file-upload' import cloneDeep from 'lodash/cloneDeep' import { expectDeepEqual } from '@opentrons/shared-data/js/cypressUtils' diff --git a/protocol-designer/cypress/integration/mixSettings.spec.js b/protocol-designer/cypress/integration/mixSettings.spec.js index 67960c5dd94..809c92237b3 100644 --- a/protocol-designer/cypress/integration/mixSettings.spec.js +++ b/protocol-designer/cypress/integration/mixSettings.spec.js @@ -1,4 +1,3 @@ -import { describe, it } from 'vitest' const isMacOSX = Cypress.platform === 'darwin' const invalidInput = 'abcdefghijklmnopqrstuvwxyz!@#$%^&*()<>?,-' const batchEditClickOptions = { [isMacOSX ? 'metaKey' : 'ctrlKey']: true } @@ -125,7 +124,7 @@ describe('Advanced Settings for Mix Form', () => { cy.get('[data-test="StepItem_2"]').click(batchEditClickOptions) cy.get('input[name="aspirate_flowRate"]').click({ force: true }) - cy.get('div[class*=FlowRateInput__description]').contains( + cy.contains( 'Our default aspirate speed is optimal for a P1000 Single-Channel GEN2 aspirating liquids with a viscosity similar to water' ) cy.get('input[name="aspirate_flowRate_customFlowRate"]').type('100') @@ -144,7 +143,7 @@ describe('Advanced Settings for Mix Form', () => { it('verify functionality of flowrate in batch edit mix form', () => { // Batch editing the Flowrate value cy.get('input[name="aspirate_flowRate"]').click({ force: true }) - cy.get('div[class*=FlowRateInput__description]').contains( + cy.contains( 'Our default aspirate speed is optimal for a P1000 Single-Channel GEN2 aspirating liquids with a viscosity similar to water' ) cy.get('input[name="aspirate_flowRate_customFlowRate"]').type('100') diff --git a/protocol-designer/cypress/integration/settings.spec.js b/protocol-designer/cypress/integration/settings.spec.js index 5e70c779ffd..3f248d79ab0 100644 --- a/protocol-designer/cypress/integration/settings.spec.js +++ b/protocol-designer/cypress/integration/settings.spec.js @@ -1,4 +1,3 @@ -import { describe, it, before } from 'vitest' describe('The Settings Page', () => { const exptlSettingText = 'Disable module placement restrictions' @@ -142,7 +141,7 @@ describe('The Settings Page', () => { cy.contains(exptlSettingText).next().click() cy.get('button').contains('Continue').click() // Leave the settings page - cy.get("button[class*='navbar__tab__']").contains('FILE').click() + cy.get("button[id='NavTab_file']").contains('FILE').click() // Go back to settings cy.openSettingsPage() // The toggle is still on @@ -160,7 +159,7 @@ describe('The Settings Page', () => { cy.contains(exptlSettingText).next().click() cy.get('button').contains('Continue').click() // Leave the settings page - cy.get("button[class*='navbar__tab__']").contains('FILE') + cy.get("button[id='NavTab_file']").contains('FILE') // Go back to settings cy.openSettingsPage() // The toggle is still off diff --git a/protocol-designer/cypress/integration/sidebar.spec.js b/protocol-designer/cypress/integration/sidebar.spec.js index e967c0c7b38..7b71fc67cc2 100644 --- a/protocol-designer/cypress/integration/sidebar.spec.js +++ b/protocol-designer/cypress/integration/sidebar.spec.js @@ -1,5 +1,3 @@ -import { describe, it, beforeEach } from 'vitest' - describe('Desktop Navigation', () => { beforeEach(() => { cy.visit('/') @@ -7,7 +5,7 @@ describe('Desktop Navigation', () => { }) it('contains a working file button', () => { - cy.get("button[class*='navbar__tab__']") + cy.get("button[id='NavTab_file']") .contains('FILE') .parent() .should('have.prop', 'disabled') @@ -15,21 +13,21 @@ describe('Desktop Navigation', () => { }) it('contains a disabled liquids button', () => { - cy.get("button[class*='navbar__tab__']") + cy.get("button[id='NavTab_liquids']") .contains('LIQUIDS') .parent() .should('have.prop', 'disabled') }) it('contains a disabled design button', () => { - cy.get("button[class*='navbar__tab__']") + cy.get("button[id='NavTab_design']") .contains('DESIGN') .parent() .should('have.prop', 'disabled') }) it('contains a help button with external link', () => { - cy.get("a[class*='navbar__tab__']") + cy.get('a') .contains('HELP') .parent() .should('have.prop', 'href') @@ -37,13 +35,11 @@ describe('Desktop Navigation', () => { }) it('contains a settings button', () => { - cy.get("button[class*='navbar__tab__']") - .contains('Settings') - .should('exist') + cy.get('button').contains('Settings').should('exist') }) it('returns to the file controls when the file button is clicked', () => { - cy.get("button[class*='navbar__tab__']").contains('FILE').click() + cy.get("button[id='NavTab_file']").contains('FILE').click() cy.contains('Protocol File') }) }) diff --git a/protocol-designer/cypress/integration/transferSettings.spec.js b/protocol-designer/cypress/integration/transferSettings.spec.js index 4cbb114a47b..a4c831fddd4 100644 --- a/protocol-designer/cypress/integration/transferSettings.spec.js +++ b/protocol-designer/cypress/integration/transferSettings.spec.js @@ -1,5 +1,3 @@ -import { describe, it, before } from 'vitest' - const isMacOSX = Cypress.platform === 'darwin' const batchEditClickOptions = { [isMacOSX ? 'metaKey' : 'ctrlKey']: true } @@ -134,7 +132,7 @@ describe('Advanced Settings for Transfer Form', () => { cy.get('[data-test="StepItem_2"]').click(batchEditClickOptions) cy.get('input[name="aspirate_flowRate"]').click({ force: true }) - cy.get('div[class*=FlowRateInput__description]').contains( + cy.contains( 'Our default aspirate speed is optimal for a P1000 Single-Channel GEN2 aspirating liquids with a viscosity similar to water' ) cy.get('input[name="aspirate_flowRate_customFlowRate"]').type('100') @@ -153,7 +151,7 @@ describe('Advanced Settings for Transfer Form', () => { it('verify functionality of flowrate in batch edit transfer', () => { // Batch editing the Flowrate value cy.get('input[name="aspirate_flowRate"]').click({ force: true }) - cy.get('div[class*=FlowRateInput__description]').contains( + cy.contains( 'Our default aspirate speed is optimal for a P1000 Single-Channel GEN2 aspirating liquids with a viscosity similar to water' ) cy.get('input[name="aspirate_flowRate_customFlowRate"]').type('100') diff --git a/protocol-designer/cypress/plugins/index.js b/protocol-designer/cypress/plugins/index.js index da99fcd4bb9..f392875c7d9 100644 --- a/protocol-designer/cypress/plugins/index.js +++ b/protocol-designer/cypress/plugins/index.js @@ -1,3 +1,5 @@ +// eslint-disable-next-line @typescript-eslint/triple-slash-reference +/// // *********************************************************** // This example plugins/index.js can be used to load plugins // @@ -7,15 +9,15 @@ // You can read more here: // https://on.cypress.io/plugins-guide // *********************************************************** -const webpackPreprocessor = require('@cypress/webpack-preprocessor') -const createWebpackConfig = require('../../webpack.config') // This function is called when a project is opened or re-opened (e.g. due to // the project's config changing) -module.exports = async (on, config) => { +/** + * @type {Cypress.PluginConfig} + */ +// eslint-disable-next-line no-unused-vars +module.exports = (on, config) => { // `on` is used to hook into various events Cypress emits // `config` is the resolved Cypress config - const webpackOptions = await createWebpackConfig() - on('file:preprocessor', webpackPreprocessor({ webpackOptions })) } diff --git a/protocol-designer/cypress/support/commands.js b/protocol-designer/cypress/support/commands.js index 6de9b96aabc..09543c42330 100644 --- a/protocol-designer/cypress/support/commands.js +++ b/protocol-designer/cypress/support/commands.js @@ -34,14 +34,17 @@ Cypress.Commands.add('closeAnnouncementModal', () => { cy.get('[data-test="ComputingSpinner"]', { timeout: 30000 }).should( 'not.exist' ) - cy.get('button').contains('Got It!').should('be.visible').click() + cy.get('button') + .contains('Got It!') + .should('be.visible') + .click({ force: true }) }) // // File Page Actions // Cypress.Commands.add('openFilePage', () => { - cy.get('button[class*="navbar__tab__"]').contains('FILE').click() + cy.get('button[id="NavTab_file"]').contains('FILE').click() }) // @@ -87,7 +90,7 @@ Cypress.Commands.add( // Design Page Actions // Cypress.Commands.add('openDesignPage', () => { - cy.get('button[class*="navbar__tab__"]').contains('DESIGN').parent().click() + cy.get('button[id="NavTab_design"]').contains('DESIGN').parent().click() }) Cypress.Commands.add('addStep', stepName => { cy.get('button').contains('Add Step').click() @@ -98,7 +101,7 @@ Cypress.Commands.add('addStep', stepName => { // Settings Page Actions // Cypress.Commands.add('openSettingsPage', () => { - cy.get('button[class*="navbar__tab__"]').contains('Settings').click() + cy.get('button').contains('Settings').click() }) // Advance Settings for Transfer Steps diff --git a/protocol-designer/index.html b/protocol-designer/index.html index cfcafbedf22..9fbcfaf5875 100644 --- a/protocol-designer/index.html +++ b/protocol-designer/index.html @@ -7,7 +7,7 @@ - Protocol Designer + Opentrons Protocol Designer
diff --git a/protocol-designer/package.json b/protocol-designer/package.json index 7e8969f5885..564ebdb2fe1 100755 --- a/protocol-designer/package.json +++ b/protocol-designer/package.json @@ -8,8 +8,7 @@ "email": "engineering@opentrons.com" }, "name": "protocol-designer", - "type": "module", - "productName": "Opentrons Protocol Designer BETA", + "productName": "Opentrons Protocol Designer", "private": true, "version": "0.0.0-dev", "description": "Protocol designer app", diff --git a/protocol-designer/src/components/DeckSetup/LabwareOverlays/AdapterControls.tsx b/protocol-designer/src/components/DeckSetup/LabwareOverlays/AdapterControls.tsx index 8f4c81491b3..172b5b1129a 100644 --- a/protocol-designer/src/components/DeckSetup/LabwareOverlays/AdapterControls.tsx +++ b/protocol-designer/src/components/DeckSetup/LabwareOverlays/AdapterControls.tsx @@ -14,7 +14,6 @@ import { moveDeckItem, openAddLabwareModal, } from '../../../labware-ingred/actions' -import { getDeckSetupForActiveItem } from '../../../top-selectors/labware-locations' import { selectors as labwareDefSelectors } from '../../../labware-defs' import { START_TERMINAL_ITEM_ID, TerminalItemId } from '../../../steplist' import { BlockedSlot } from './BlockedSlot' @@ -54,68 +53,59 @@ export const AdapterControls = ( const customLabwareDefs = useSelector( labwareDefSelectors.getCustomLabwareDefsByURI ) - const activeDeckSetup = useSelector(getDeckSetupForActiveItem) - const labware = activeDeckSetup.labware const ref = React.useRef(null) - const [newSlot, setSlot] = React.useState(null) const dispatch = useDispatch() const adapterName = allLabware.find(labware => labware.id === labwareId)?.def.metadata .displayName ?? '' - const [{ itemType, draggedItem, isOver }, drop] = useDrop({ - accept: DND_TYPES.LABWARE, - canDrop: (item: DroppedItem) => { - const draggedDef = item.labwareOnDeck?.def - assert(draggedDef, 'no labware def of dragged item, expected it on drop') - - if (draggedDef != null) { - const isCustomLabware = getLabwareIsCustom( - customLabwareDefs, - item.labwareOnDeck - ) - return ( - getAdapterLabwareIsAMatch( - labwareId, - allLabware, - draggedDef.parameters.loadName - ) || isCustomLabware + const [{ itemType, draggedItem, isOver }, drop] = useDrop( + () => ({ + accept: DND_TYPES.LABWARE, + canDrop: (item: DroppedItem) => { + const draggedDef = item.labwareOnDeck?.def + console.assert( + draggedDef, + 'no labware def of dragged item, expected it on drop' ) - } - return true - }, - drop: (item: DroppedItem) => { - const droppedLabware = item - if (newSlot != null) { - dispatch(moveDeckItem(newSlot, labwareId)) - } else if (droppedLabware.labwareOnDeck != null) { - const droppedSlot = droppedLabware.labwareOnDeck.slot - dispatch(moveDeckItem(droppedSlot, labwareId)) - } - }, - hover: () => { - if (handleDragHover != null) { - handleDragHover() - } - }, - collect: (monitor: DropTargetMonitor) => ({ - itemType: monitor.getItemType(), - isOver: !!monitor.isOver(), - draggedItem: monitor.getItem() as DroppedItem, - }), - }) - const draggedLabware = Object.values(labware).find( - l => l.id === draggedItem?.labwareOnDeck?.id + if (draggedDef != null) { + const isCustomLabware = getLabwareIsCustom( + customLabwareDefs, + item.labwareOnDeck + ) + return ( + getAdapterLabwareIsAMatch( + labwareId, + allLabware, + draggedDef.parameters.loadName + ) || isCustomLabware + ) + } + return true + }, + drop: (item: DroppedItem) => { + const droppedLabware = item + if (droppedLabware.labwareOnDeck != null) { + const droppedSlot = droppedLabware.labwareOnDeck.slot + dispatch(moveDeckItem(droppedSlot, labwareId)) + } + }, + hover: () => { + if (handleDragHover != null) { + handleDragHover() + } + }, + collect: (monitor: DropTargetMonitor) => ({ + itemType: monitor.getItemType(), + isOver: !!monitor.isOver(), + draggedItem: monitor.getItem() as DroppedItem, + }), + }), + [] ) - React.useEffect(() => { - if (draggedLabware != null) { - setSlot(draggedLabware.slot) - } - }) - if ( selectedTerminalItemId !== START_TERMINAL_ITEM_ID || (itemType !== DND_TYPES.LABWARE && itemType !== null) diff --git a/protocol-designer/src/components/DeckSetup/LabwareOverlays/EditLabware.tsx b/protocol-designer/src/components/DeckSetup/LabwareOverlays/EditLabware.tsx index 74131a71dbe..d3c5e72270d 100644 --- a/protocol-designer/src/components/DeckSetup/LabwareOverlays/EditLabware.tsx +++ b/protocol-designer/src/components/DeckSetup/LabwareOverlays/EditLabware.tsx @@ -7,7 +7,6 @@ import { getLabwareDisplayName } from '@opentrons/shared-data' import { DropTargetMonitor, useDrag, useDrop } from 'react-dnd' import { NameThisLabware } from './NameThisLabware' import { DND_TYPES } from '../../../constants' -import { getDeckSetupForActiveItem } from '../../../top-selectors/labware-locations' import { deleteContainer, duplicateLabware, @@ -39,10 +38,7 @@ export const EditLabware = (props: Props): JSX.Element | null => { const savedLabware = useSelector(labwareIngredSelectors.getSavedLabware) const dispatch = useDispatch>() const { t } = useTranslation('deck') - const activeDeckSetup = useSelector(getDeckSetupForActiveItem) - const labware = activeDeckSetup.labware const ref = React.useRef(null) - const [newSlot, setSlot] = React.useState(null) const { isTiprack } = labwareOnDeck.def.parameters const hasName = savedLabware[labwareOnDeck.id] @@ -52,47 +48,46 @@ export const EditLabware = (props: Props): JSX.Element | null => { dispatch(openIngredientSelector(labwareOnDeck.id)) } - const [, drag] = useDrag({ - type: DND_TYPES.LABWARE, - item: { labwareOnDeck }, - }) + const [, drag] = useDrag( + () => ({ + type: DND_TYPES.LABWARE, + item: { labwareOnDeck }, + }), + [labwareOnDeck] + ) - const [{ draggedLabware, isOver }, drop] = useDrop(() => ({ - accept: DND_TYPES.LABWARE, - canDrop: (item: DroppedItem) => { - const draggedLabware = item?.labwareOnDeck - const isDifferentSlot = - draggedLabware && draggedLabware.slot !== labwareOnDeck.slot - return isDifferentSlot && !swapBlocked - }, - drop: (item: DroppedItem) => { - const draggedLabware = item?.labwareOnDeck - if (newSlot != null) { - dispatch(moveDeckItem(newSlot, labwareOnDeck.slot)) - } else if (draggedLabware != null) { - dispatch(moveDeckItem(draggedLabware.slot, labwareOnDeck.slot)) - } - }, + const [{ draggedLabware, isOver }, drop] = useDrop( + () => ({ + accept: DND_TYPES.LABWARE, + canDrop: (item: DroppedItem) => { + const draggedLabware = item?.labwareOnDeck + const isDifferentSlot = + draggedLabware && draggedLabware.slot !== labwareOnDeck.slot + return isDifferentSlot && !swapBlocked + }, + drop: (item: DroppedItem) => { + const draggedLabware = item?.labwareOnDeck + if (draggedLabware != null) { + dispatch(moveDeckItem(draggedLabware.slot, labwareOnDeck.slot)) + } + }, - hover: (item: DroppedItem, monitor: DropTargetMonitor) => { - if (monitor.canDrop()) { - setHoveredLabware(labwareOnDeck) - } - }, - collect: (monitor: DropTargetMonitor) => ({ - isOver: monitor.isOver(), - draggedLabware: monitor.getItem() as DroppedItem, + hover: (item: DroppedItem, monitor: DropTargetMonitor) => { + if (monitor.canDrop()) { + setHoveredLabware(labwareOnDeck) + } + }, + collect: (monitor: DropTargetMonitor) => ({ + isOver: monitor.isOver(), + draggedLabware: monitor.getItem() as DroppedItem, + }), }), - })) - - const draggedItem = Object.values(labware).find( - l => l.id === draggedLabware?.labwareOnDeck?.id + [labwareOnDeck] ) React.useEffect(() => { - if (draggedItem != null) { - setSlot(draggedItem.slot) - setDraggedLabware(draggedItem) + if (draggedLabware?.labwareOnDeck != null) { + setDraggedLabware(draggedLabware?.labwareOnDeck) } else { setHoveredLabware(null) setDraggedLabware(null) @@ -107,7 +102,8 @@ export const EditLabware = (props: Props): JSX.Element | null => { /> ) } else { - const isBeingDragged = draggedItem?.slot === labwareOnDeck.slot + const isBeingDragged = + draggedLabware?.labwareOnDeck?.slot === labwareOnDeck.slot let contents: React.ReactNode | null = null diff --git a/protocol-designer/src/components/DeckSetup/LabwareOverlays/SlotControls.tsx b/protocol-designer/src/components/DeckSetup/LabwareOverlays/SlotControls.tsx index 14a27061cb3..2849506cefb 100644 --- a/protocol-designer/src/components/DeckSetup/LabwareOverlays/SlotControls.tsx +++ b/protocol-designer/src/components/DeckSetup/LabwareOverlays/SlotControls.tsx @@ -14,7 +14,6 @@ import { moveDeckItem, openAddLabwareModal, } from '../../../labware-ingred/actions' -import { getDeckSetupForActiveItem } from '../../../top-selectors/labware-locations' import { selectors as labwareDefSelectors } from '../../../labware-defs' import { START_TERMINAL_ITEM_ID, TerminalItemId } from '../../../steplist' import { BlockedSlot } from './BlockedSlot' @@ -53,10 +52,7 @@ export const SlotControls = (props: SlotControlsProps): JSX.Element | null => { const customLabwareDefs = useSelector( labwareDefSelectors.getCustomLabwareDefsByURI ) - const activeDeckSetup = useSelector(getDeckSetupForActiveItem) - const labware = activeDeckSetup.labware const ref = React.useRef(null) - const [newSlot, setSlot] = React.useState(null) const dispatch = useDispatch() const { t } = useTranslation('deck') @@ -66,54 +62,50 @@ export const SlotControls = (props: SlotControlsProps): JSX.Element | null => { item: { labwareOnDeck: null }, }) - const [{ draggedItem, itemType, isOver }, drop] = useDrop({ - accept: DND_TYPES.LABWARE, - canDrop: (item: DroppedItem) => { - const draggedDef = item?.labwareOnDeck?.def - assert(draggedDef, 'no labware def of dragged item, expected it on drop') - - if (moduleType != null && draggedDef != null) { - // this is a module slot, prevent drop if the dragged labware is not compatible - const isCustomLabware = getLabwareIsCustom( - customLabwareDefs, - item.labwareOnDeck + const [{ draggedItem, itemType, isOver }, drop] = useDrop( + () => ({ + accept: DND_TYPES.LABWARE, + canDrop: (item: DroppedItem) => { + const draggedDef = item?.labwareOnDeck?.def + console.assert( + draggedDef, + 'no labware def of dragged item, expected it on drop' ) - return getLabwareIsCompatible(draggedDef, moduleType) || isCustomLabware - } - return true - }, - drop: (item: DroppedItem) => { - const droppedLabware = item - if (newSlot != null) { - dispatch(moveDeckItem(newSlot, slotId)) - } else if (droppedLabware.labwareOnDeck != null) { - const droppedSlot = droppedLabware.labwareOnDeck.slot - dispatch(moveDeckItem(droppedSlot, slotId)) - } - }, - hover: () => { - if (handleDragHover != null) { - handleDragHover() - } - }, - collect: (monitor: DropTargetMonitor) => ({ - itemType: monitor.getItemType(), - isOver: !!monitor.isOver(), - draggedItem: monitor.getItem() as DroppedItem, + if (moduleType != null && draggedDef != null) { + // this is a module slot, prevent drop if the dragged labware is not compatible + const isCustomLabware = getLabwareIsCustom( + customLabwareDefs, + item.labwareOnDeck + ) + + return ( + getLabwareIsCompatible(draggedDef, moduleType) || isCustomLabware + ) + } + return true + }, + drop: (item: DroppedItem) => { + const droppedLabware = item + if (droppedLabware.labwareOnDeck != null) { + const droppedSlot = droppedLabware.labwareOnDeck.slot + dispatch(moveDeckItem(droppedSlot, slotId)) + } + }, + hover: () => { + if (handleDragHover != null) { + handleDragHover() + } + }, + collect: (monitor: DropTargetMonitor) => ({ + itemType: monitor.getItemType(), + isOver: !!monitor.isOver(), + draggedItem: monitor.getItem() as DroppedItem, + }), }), - }) - - const draggedLabware = Object.values(labware).find( - l => l.id === draggedItem?.labwareOnDeck?.id + [] ) - React.useEffect(() => { - if (draggedLabware != null) { - setSlot(draggedLabware.slot) - } - }) - if ( selectedTerminalItemId !== START_TERMINAL_ITEM_ID || (itemType !== DND_TYPES.LABWARE && itemType !== null) || diff --git a/protocol-designer/src/components/FileSidebar/FileSidebar.tsx b/protocol-designer/src/components/FileSidebar/FileSidebar.tsx index 00dd7b3765f..3049f036b4a 100644 --- a/protocol-designer/src/components/FileSidebar/FileSidebar.tsx +++ b/protocol-designer/src/components/FileSidebar/FileSidebar.tsx @@ -243,7 +243,8 @@ export function v8WarningContent(t: any): JSX.Element { } export function FileSidebar(): JSX.Element { const fileData = useSelector(fileDataSelectors.createFile) - const canDownload = useSelector(selectors.getCurrentPage) + const currentPage = useSelector(selectors.getCurrentPage) + const canDownload = currentPage !== 'file-splash' const initialDeckSetup = useSelector(stepFormSelectors.getInitialDeckSetup) const modulesOnDeck = initialDeckSetup.modules const pipettesOnDeck = initialDeckSetup.pipettes diff --git a/protocol-designer/src/components/steplist/DraggableStepItems.tsx b/protocol-designer/src/components/steplist/DraggableStepItems.tsx index e8c3ef0c22a..0eadb1ce5a7 100644 --- a/protocol-designer/src/components/steplist/DraggableStepItems.tsx +++ b/protocol-designer/src/components/steplist/DraggableStepItems.tsx @@ -7,7 +7,6 @@ import { useDrag, DropTargetOptions, } from 'react-dnd' -import isEqual from 'lodash/isEqual' import { DND_TYPES } from '../../constants' import { selectors as stepFormSelectors } from '../../step-forms' @@ -22,10 +21,9 @@ import styles from './StepItem.module.css' interface DragDropStepItemProps extends ConnectedStepItemProps { stepId: StepIdType - clickDrop: () => void moveStep: (stepId: StepIdType, value: number) => void - setIsOver: React.Dispatch> findStepIndex: (stepId: StepIdType) => number + orderedStepIds: string[] } interface DropType { @@ -33,41 +31,39 @@ interface DropType { } const DragDropStepItem = (props: DragDropStepItemProps): JSX.Element => { - const { stepId, moveStep, clickDrop, setIsOver, findStepIndex } = props + const { stepId, moveStep, findStepIndex, orderedStepIds } = props const ref = React.useRef(null) - const [{ isDragging }, drag] = useDrag({ - type: DND_TYPES.STEP_ITEM, - item: { stepId }, - collect: (monitor: DragLayerMonitor) => ({ - isDragging: monitor.isDragging(), - }), - }) - - const [{ isOver, handlerId }, drop] = useDrop(() => ({ - accept: DND_TYPES.STEP_ITEM, - canDrop: () => { - return true - }, - drop: () => { - clickDrop() - }, - hover: (item: DropType) => { - const draggedId = item.stepId - if (draggedId !== stepId) { - const overIndex = findStepIndex(stepId) - moveStep(draggedId, overIndex) - } - }, - collect: (monitor: DropTargetOptions) => ({ - isOver: monitor.isOver(), - handlerId: monitor.getHandlerId(), + const [{ isDragging }, drag] = useDrag( + () => ({ + type: DND_TYPES.STEP_ITEM, + item: { stepId }, + collect: (monitor: DragLayerMonitor) => ({ + isDragging: monitor.isDragging(), + }), }), - })) + [orderedStepIds] + ) - React.useEffect(() => { - setIsOver(isOver) - }, [isOver]) + const [{ handlerId }, drop] = useDrop( + () => ({ + accept: DND_TYPES.STEP_ITEM, + canDrop: () => { + return true + }, + drop: (item: DropType) => { + const draggedId = item.stepId + if (draggedId !== stepId) { + const overIndex = findStepIndex(stepId) + moveStep(draggedId, overIndex) + } + }, + collect: (monitor: DropTargetOptions) => ({ + handlerId: monitor.getHandlerId(), + }), + }), + [orderedStepIds] + ) drag(drop(ref)) return ( @@ -90,42 +86,32 @@ export const DraggableStepItems = ( ): JSX.Element | null => { const { orderedStepIds, reorderSteps } = props const { t } = useTranslation('shared') - const [isOver, setIsOver] = React.useState(false) - const [stepIds, setStepIds] = React.useState(orderedStepIds) - - // needed to initalize stepIds - React.useEffect(() => { - setStepIds(orderedStepIds) - }, [orderedStepIds]) - - const clickDrop = (): void => { - if (!isEqual(orderedStepIds, stepIds)) { - if (confirm(t('confirm_reorder'))) { - reorderSteps(stepIds) - } - } - } const findStepIndex = (stepId: StepIdType): number => - stepIds.findIndex(id => stepId === id) + orderedStepIds.findIndex(id => stepId === id) const moveStep = (stepId: StepIdType, targetIndex: number): void => { - const currentIndex = orderedStepIds.findIndex(id => id === stepId) - - const newStepIds = [...orderedStepIds] - newStepIds.splice(currentIndex, 1) - newStepIds.splice(targetIndex, 0, stepId) - - setStepIds(newStepIds) + const currentIndex = findStepIndex(stepId) + + const currentRemoved = [ + ...orderedStepIds.slice(0, currentIndex), + ...orderedStepIds.slice(currentIndex + 1, orderedStepIds.length), + ] + const currentReinserted = [ + ...currentRemoved.slice(0, targetIndex), + stepId, + ...currentRemoved.slice(targetIndex, currentRemoved.length), + ] + if (confirm(t('confirm_reorder'))) { + reorderSteps(currentReinserted) + } } - const currentIds = isOver ? stepIds : orderedStepIds - return ( <> {({ makeStepOnContextMenu }) => - currentIds.map((stepId: StepIdType, index: number) => ( + orderedStepIds.map((stepId: StepIdType, index: number) => ( )) } diff --git a/protocol-designer/src/index.hbs b/protocol-designer/src/index.hbs deleted file mode 100644 index ab68be76554..00000000000 --- a/protocol-designer/src/index.hbs +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - {{htmlWebpackPlugin.options.title}} - - - - -
- - diff --git a/protocol-designer/src/labware-defs/actions.ts b/protocol-designer/src/labware-defs/actions.ts index 33e855dc1a7..20959a37b5d 100644 --- a/protocol-designer/src/labware-defs/actions.ts +++ b/protocol-designer/src/labware-defs/actions.ts @@ -7,7 +7,7 @@ import { getLabwareDefURI, getIsTiprack, OPENTRONS_LABWARE_NAMESPACE, - protocolSchemaV2, + labwareSchemaV2, } from '@opentrons/shared-data' import { getAllWellSetsForLabware } from '../utils' import * as labwareDefSelectors from './selectors' @@ -55,8 +55,7 @@ const ajv = new Ajv({ allErrors: true, jsonPointers: true, }) -const validate = ajv.compile(protocolSchemaV2) - +const validate = ajv.compile(labwareSchemaV2) const _labwareDefsMatchingLoadName = ( labwareDefs: LabwareDefinition2[], loadName: string diff --git a/protocol-designer/src/localization/en/modal.json b/protocol-designer/src/localization/en/modal.json index 03562ba1bb3..edceb80718f 100644 --- a/protocol-designer/src/localization/en/modal.json +++ b/protocol-designer/src/localization/en/modal.json @@ -202,7 +202,7 @@ } }, "gate": { - "sign_up_below": "Sign Up For Opentrons Protocol Designer Beta", + "sign_up_below": "Sign Up For Opentrons Protocol Designer", "failed_verification": "Something Went Wrong", "sign_up_success": "Please confirm your email address to continue", "check_email": "We've sent a confirmation URL to your email that will take you to the Protocol Designer. Keep an eye out for a follow up email which contains links to resources such as our help documents." diff --git a/protocol-designer/src/timelineMiddleware/makeTimelineMiddleware.ts b/protocol-designer/src/timelineMiddleware/makeTimelineMiddleware.ts index ed7863a8208..9d9d2f399a9 100644 --- a/protocol-designer/src/timelineMiddleware/makeTimelineMiddleware.ts +++ b/protocol-designer/src/timelineMiddleware/makeTimelineMiddleware.ts @@ -102,7 +102,6 @@ export const makeTimelineMiddleware: () => Middleware = () => { if (prevTimelineArgs !== null && prevSubstepsArgs !== null) { const timelineArgs: GenerateRobotStateTimelineArgs = prevTimelineArgs const substepsArgs: SubstepsArgsNoTimeline = prevSubstepsArgs - console.log('about to post worker message') worker.postMessage({ needsTimeline: true, timelineArgs, diff --git a/protocol-designer/src/utils/labwareModuleCompatibility.ts b/protocol-designer/src/utils/labwareModuleCompatibility.ts index 57b6e3cc5bf..5092afd6903 100644 --- a/protocol-designer/src/utils/labwareModuleCompatibility.ts +++ b/protocol-designer/src/utils/labwareModuleCompatibility.ts @@ -114,6 +114,7 @@ export const COMPATIBLE_LABWARE_ALLOWLIST_FOR_ADAPTER: Record< [ALUMINUM_BLOCK_96_LOADNAME]: [ 'opentrons/biorad_96_wellplate_200ul_pcr/2', 'opentrons/nest_96_wellplate_100ul_pcr_full_skirt/2', + 'opentrons/opentrons_96_wellplate_200ul_pcr_full_skirt/2', ], [ALUMINUM_FLAT_BOTTOM_PLATE]: [ 'opentrons/corning_384_wellplate_112ul_flat/2', @@ -196,8 +197,7 @@ export const getAdapterLabwareIsAMatch = ( draggedLabwareLoadname === 'corning_96_wellplate_360ul_flat') const aluminumBlock96Pairs = loadName === ALUMINUM_BLOCK_96_LOADNAME && - (draggedLabwareLoadname === 'biorad_96_wellplate_200ul_pcr' || - draggedLabwareLoadname === 'nest_96_wellplate_100ul_pcr_full_skirt') + pcrLabwares.includes(draggedLabwareLoadname) const aluminumFlatBottomPlatePairs = loadName === ALUMINUM_FLAT_BOTTOM_PLATE && flatBottomLabwares.includes(draggedLabwareLoadname) diff --git a/protocol-designer/vite.config.ts b/protocol-designer/vite.config.ts index 7907df0b4b8..70d055a6fd8 100644 --- a/protocol-designer/vite.config.ts +++ b/protocol-designer/vite.config.ts @@ -1,58 +1,74 @@ import path from 'path' -import { defineConfig } from 'vite' +import { UserConfig, defineConfig } from 'vite' import react from '@vitejs/plugin-react' import postCssImport from 'postcss-import' import postCssApply from 'postcss-apply' import postColorModFunction from 'postcss-color-mod-function' import postCssPresetEnv from 'postcss-preset-env' import lostCss from 'lost' +import { versionForProject } from '../scripts/git-version' -export default defineConfig({ - // this makes imports relative rather than absolute - base: '', - build: { - // Relative to the root - outDir: 'dist', - }, - plugins: [ - react({ - include: '**/*.tsx', - babel: { - // Use babel.config.js files - configFile: true, +const testAliases: {} | { 'file-saver': string } = + process.env.CYPRESS === '1' + ? { + 'file-saver': + path.resolve(__dirname, 'cypress/mocks/file-saver.js') ?? '', + } + : {} + +export default defineConfig( + async (): Promise => { + const OT_PD_VERSION = await versionForProject('protocol-designer') + const OT_PD_BUILD_DATE = new Date().toUTCString() + return { + // this makes imports relative rather than absolute + base: '', + build: { + // Relative to the root + outDir: 'dist', }, - }), - ], - optimizeDeps: { - esbuildOptions: { - target: 'es2020', - }, - }, - css: { - postcss: { plugins: [ - postCssImport({ root: 'src/' }), - postCssApply(), - postColorModFunction(), - postCssPresetEnv({ stage: 0 }), - lostCss(), + react({ + include: '**/*.tsx', + babel: { + // Use babel.config.js files + configFile: true, + }, + }), ], - }, - }, - define: { - 'process.env': process.env, - global: 'globalThis', - }, - resolve: { - alias: { - '@opentrons/components/styles': path.resolve( - '../components/src/index.module.css' - ), - '@opentrons/components': path.resolve('../components/src/index.ts'), - '@opentrons/shared-data': path.resolve('../shared-data/js/index.ts'), - '@opentrons/step-generation': path.resolve( - '../step-generation/src/index.ts' - ), - }, - }, -}) + optimizeDeps: { + esbuildOptions: { + target: 'es2020', + }, + }, + css: { + postcss: { + plugins: [ + postCssImport({ root: 'src/' }), + postCssApply(), + postColorModFunction(), + postCssPresetEnv({ stage: 0 }), + lostCss(), + ], + }, + }, + define: { + 'process.env': { ...process.env, OT_PD_VERSION, OT_PD_BUILD_DATE }, + global: 'globalThis', + }, + resolve: { + alias: { + '@opentrons/components/styles': path.resolve( + '../components/src/index.module.css' + ), + '@opentrons/components': path.resolve('../components/src/index.ts'), + '@opentrons/shared-data': path.resolve('../shared-data/js/index.ts'), + '@opentrons/step-generation': path.resolve( + '../step-generation/src/index.ts' + ), + ...testAliases, + }, + }, + } + } +) diff --git a/protocol-designer/webpack.config.js b/protocol-designer/webpack.config.js deleted file mode 100644 index d987a780b31..00000000000 --- a/protocol-designer/webpack.config.js +++ /dev/null @@ -1,87 +0,0 @@ -'use strict' - -const path = require('path') -const webpack = require('webpack') -const merge = require('webpack-merge') -const HtmlWebpackPlugin = require('html-webpack-plugin') -const ScriptExtHtmlWebpackPlugin = require('script-ext-html-webpack-plugin') -const WorkerPlugin = require('worker-plugin') -const { versionForProject } = require('../scripts/git-version') - -const { DEV_MODE, baseConfig } = require('@opentrons/webpack-config') -const { productName: title, description, author } = require('./package.json') -const PROTOCOL_DESIGNER_ENV_VAR_PREFIX = 'OT_PD_' -const PASS_THROUGH_ENV_VARS = Object.keys(process.env) - .filter(v => v.startsWith(PROTOCOL_DESIGNER_ENV_VAR_PREFIX)) - .concat(['NODE_ENV', 'CYPRESS']) - -const OT_PD_BUILD_DATE = new Date().toUTCString() - -const JS_ENTRY = path.join(__dirname, 'src/index.tsx') -const HTML_ENTRY = path.join(__dirname, 'src/index.hbs') -const ERROR_HTML = path.join(__dirname, 'src/error.html') - -const OUTPUT_PATH = path.join(__dirname, 'dist') -const PUBLIC_PATH = DEV_MODE ? '' : './' - -const testAliases = - process.env.CYPRESS === '1' - ? { - 'file-saver': path.resolve(__dirname, 'cypress/mocks/file-saver.js'), - } - : {} - -module.exports = async () => { - const OT_PD_VERSION = await versionForProject('protocol-designer') - - const envVarsWithDefaults = { - OT_PD_VERSION, - OT_PD_BUILD_DATE, - } - - const envVars = PASS_THROUGH_ENV_VARS.reduce( - (acc, envVar) => ({ [envVar]: '', ...acc }), - { ...envVarsWithDefaults } - ) - console.log(`PD version: ${OT_PD_VERSION || 'UNKNOWN!'}`) - return merge(baseConfig, { - entry: [JS_ENTRY], - - output: Object.assign( - { - path: OUTPUT_PATH, - publicPath: PUBLIC_PATH, - }, - // workaround for worker-plugin HMR - // see https://github.com/GoogleChromeLabs/worker-plugin#globalobject-string--false - DEV_MODE ? { globalObject: 'this' } : {} - ), - - plugins: [ - new webpack.EnvironmentPlugin(envVars), - new WorkerPlugin({ - // disable warnings about HMR when we're in prod - globalObject: DEV_MODE ? 'self' : false, - // add required JS plugins to child compiler - plugins: ['EnvironmentPlugin'], - }), - new HtmlWebpackPlugin({ - title, - description, - author, - template: HTML_ENTRY, - favicon: './src/images/favicon.ico', - }), - new HtmlWebpackPlugin({ - filename: 'error.html', - inject: false, - template: ERROR_HTML, - }), - new ScriptExtHtmlWebpackPlugin({ defaultAttribute: 'defer' }), - ], - - resolve: { - alias: testAliases, - }, - }) -} diff --git a/robot-server/robot_server/runs/router/commands_router.py b/robot-server/robot_server/runs/router/commands_router.py index f3f81a7751c..734d1a26066 100644 --- a/robot-server/robot_server/runs/router/commands_router.py +++ b/robot-server/robot_server/runs/router/commands_router.py @@ -296,6 +296,7 @@ async def get_run_commands( completedAt=c.completedAt, params=c.params, error=c.error, + notes=c.notes, ) for c in command_slice.commands ] diff --git a/robot-server/robot_server/runs/run_models.py b/robot-server/robot_server/runs/run_models.py index ee85902440a..85a1446b631 100644 --- a/robot-server/robot_server/runs/run_models.py +++ b/robot-server/robot_server/runs/run_models.py @@ -16,6 +16,7 @@ LabwareOffset, LabwareOffsetCreate, Liquid, + CommandNote, ) from opentrons_shared_data.errors import GeneralError from robot_server.service.json_api import ResourceModel @@ -56,6 +57,10 @@ class RunCommandSummary(ResourceModel): None, description="Why this command was added to the run.", ) + notes: Optional[List[CommandNote]] = Field( + None, + description="Notes pertaining to this command.", + ) class Run(ResourceModel): diff --git a/robot-server/robot_server/runs/run_store.py b/robot-server/robot_server/runs/run_store.py index 38df8e064c6..a6da6942a11 100644 --- a/robot-server/robot_server/runs/run_store.py +++ b/robot-server/robot_server/runs/run_store.py @@ -131,7 +131,7 @@ def update_run_state( action_rows = transaction.execute(select_actions).all() self._clear_caches() - self._runs_publisher.publish_runs(run_id=run_id) + self._runs_publisher.publish_runs_advise_refetch(run_id=run_id) return _convert_row_to_run(row=run_row, action_rows=action_rows) def insert_action(self, run_id: str, action: RunAction) -> None: @@ -154,7 +154,7 @@ def insert_action(self, run_id: str, action: RunAction) -> None: transaction.execute(insert) self._clear_caches() - self._runs_publisher.publish_runs(run_id=run_id) + self._runs_publisher.publish_runs_advise_refetch(run_id=run_id) def insert( self, @@ -196,7 +196,7 @@ def insert( raise ProtocolNotFoundError(protocol_id=run.protocol_id) self._clear_caches() - self._runs_publisher.publish_runs(run_id=run_id) + self._runs_publisher.publish_runs_advise_refetch(run_id=run_id) return run @lru_cache(maxsize=_CACHE_ENTRIES) @@ -417,7 +417,7 @@ def remove(self, run_id: str) -> None: raise RunNotFoundError(run_id) self._clear_caches() - self._runs_publisher.publish_runs(run_id=run_id) + self._runs_publisher.publish_runs_advise_unsubscribe(run_id=run_id) def _run_exists( self, run_id: str, connection: sqlalchemy.engine.Connection diff --git a/robot-server/robot_server/service/json_api/__init__.py b/robot-server/robot_server/service/json_api/__init__.py index 8966763cb53..2680c99049f 100644 --- a/robot-server/robot_server/service/json_api/__init__.py +++ b/robot-server/robot_server/service/json_api/__init__.py @@ -16,6 +16,7 @@ PydanticResponse, ResponseList, NotifyRefetchBody, + NotifyUnsubscribeBody, ) @@ -46,4 +47,5 @@ "ResponseList", # notify models "NotifyRefetchBody", + "NotifyUnsubscribeBody", ] diff --git a/robot-server/robot_server/service/json_api/response.py b/robot-server/robot_server/service/json_api/response.py index dd2d0dc7b1d..9d2c2cb76b9 100644 --- a/robot-server/robot_server/service/json_api/response.py +++ b/robot-server/robot_server/service/json_api/response.py @@ -285,5 +285,15 @@ class ResponseList(BaseModel, Generic[ResponseDataT]): class NotifyRefetchBody(BaseResponseBody): - "A notification response that returns a flag for refetching via HTTP." + """A notification response that returns a flag for refetching via HTTP.""" + refetchUsingHTTP: bool = True + + +class NotifyUnsubscribeBody(BaseResponseBody): + """A notification response. + + Returns flags for unsubscribing from a topic. + """ + + unsubscribe: bool = True diff --git a/robot-server/robot_server/service/notifications/notification_client.py b/robot-server/robot_server/service/notifications/notification_client.py index 1ca2703d031..568d161cf53 100644 --- a/robot-server/robot_server/service/notifications/notification_client.py +++ b/robot-server/robot_server/service/notifications/notification_client.py @@ -6,7 +6,7 @@ from typing import Any, Dict, Optional from enum import Enum -from ..json_api import NotifyRefetchBody +from ..json_api import NotifyRefetchBody, NotifyUnsubscribeBody from server_utils.fastapi_utils.app_state import ( AppState, AppStateAccessor, @@ -77,26 +77,50 @@ async def disconnect(self) -> None: self.client.loop_stop() await to_thread.run_sync(self.client.disconnect) - async def publish_async( - self, topic: str, message: NotifyRefetchBody = NotifyRefetchBody() + async def publish_advise_refetch_async(self, topic: str) -> None: + """Asynchronously publish a refetch message on a specific topic to the MQTT broker. + + Args: + topic: The topic to publish the message on. + """ + await to_thread.run_sync(self.publish_advise_refetch, topic) + + async def publish_advise_unsubscribe_async(self, topic: str) -> None: + """Asynchronously publish an unsubscribe message on a specific topic to the MQTT broker. + + Args: + topic: The topic to publish the message on. + """ + await to_thread.run_sync(self.publish_advise_unsubscribe, topic) + + def publish_advise_refetch( + self, + topic: str, ) -> None: - """Asynchronously Publish a message on a specific topic to the MQTT broker. + """Publish a refetch message on a specific topic to the MQTT broker. Args: topic: The topic to publish the message on. - message: The message to be published, in the format of NotifyRefetchBody. """ - await to_thread.run_sync(self.publish, topic, message) + message = NotifyRefetchBody.construct() + payload = message.json() + self.client.publish( + topic=topic, + payload=payload, + qos=self._default_qos, + retain=self._retain_message, + ) - def publish( - self, topic: str, message: NotifyRefetchBody = NotifyRefetchBody() + def publish_advise_unsubscribe( + self, + topic: str, ) -> None: - """Publish a message on a specific topic to the MQTT broker. + """Publish an unsubscribe message on a specific topic to the MQTT broker. Args: topic: The topic to publish the message on. - message: The message to be published. """ + message = NotifyUnsubscribeBody.construct() payload = message.json() self.client.publish( topic=topic, diff --git a/robot-server/robot_server/service/notifications/publishers/maintenance_runs_publisher.py b/robot-server/robot_server/service/notifications/publishers/maintenance_runs_publisher.py index f6f146e11e4..8ef07fd7eac 100644 --- a/robot-server/robot_server/service/notifications/publishers/maintenance_runs_publisher.py +++ b/robot-server/robot_server/service/notifications/publishers/maintenance_runs_publisher.py @@ -20,7 +20,9 @@ async def publish_current_maintenance_run( self, ) -> None: """Publishes the equivalent of GET /maintenance_run/current_run""" - await self._client.publish_async(topic=Topics.MAINTENANCE_RUNS_CURRENT_RUN) + await self._client.publish_advise_refetch_async( + topic=Topics.MAINTENANCE_RUNS_CURRENT_RUN + ) _maintenance_runs_publisher_accessor: AppStateAccessor[ diff --git a/robot-server/robot_server/service/notifications/publishers/runs_publisher.py b/robot-server/robot_server/service/notifications/publishers/runs_publisher.py index 11222005b05..94aed694e8f 100644 --- a/robot-server/robot_server/service/notifications/publishers/runs_publisher.py +++ b/robot-server/robot_server/service/notifications/publishers/runs_publisher.py @@ -1,5 +1,6 @@ from fastapi import Depends import asyncio +import logging from typing import Union, Callable, Optional from opentrons.protocol_engine import CurrentCommand, StateSummary, EngineStatus @@ -13,6 +14,11 @@ from ..topics import Topics +log: logging.Logger = logging.getLogger(__name__) + +POLL_INTERVAL = 1 + + class RunsPublisher: """Publishes protocol runs topics.""" @@ -34,7 +40,8 @@ async def begin_polling_engine_store( """Continuously poll the engine store for the current_command. Args: - current_command: The currently executing command, if any. + get_current_command: Callback to get the currently executing command, if any. + get_state_summary: Callback to get the current run's state summary, if any. run_id: ID of the current run. """ if self._poller is None: @@ -56,24 +63,28 @@ async def begin_polling_engine_store( ) async def stop_polling_engine_store(self) -> None: - """Stops polling the engine store.""" + """Stops polling the engine store. Run-related topics will publish as the poller is cancelled.""" if self._poller is not None: self._run_data_manager_polling.set() self._poller.cancel() - self._poller = None - self._run_data_manager_polling.clear() - self._previous_current_command = None - self._previous_state_summary_status = None - await self._client.publish_async(topic=Topics.RUNS_CURRENT_COMMAND) - def publish_runs(self, run_id: str) -> None: + def publish_runs_advise_refetch(self, run_id: str) -> None: + """Publishes the equivalent of GET /runs and GET /runs/:runId. + + Args: + run_id: ID of the current run. + """ + self._client.publish_advise_refetch(topic=Topics.RUNS) + self._client.publish_advise_refetch(topic=f"{Topics.RUNS}/{run_id}") + + def publish_runs_advise_unsubscribe(self, run_id: str) -> None: """Publishes the equivalent of GET /runs and GET /runs/:runId. Args: run_id: ID of the current run. """ - self._client.publish(topic=Topics.RUNS) - self._client.publish(topic=f"{Topics.RUNS}/{run_id}") + self._client.publish_advise_unsubscribe(topic=Topics.RUNS) + self._client.publish_advise_unsubscribe(topic=f"{Topics.RUNS}/{run_id}") async def _poll_engine_store( self, @@ -85,8 +96,38 @@ async def _poll_engine_store( Args: get_current_command: Retrieves the engine store's current command. + get_state_summary: Retrieves the engine store's state summary. run_id: ID of the current run. """ + try: + await self._poll_for_run_id_info( + get_current_command=get_current_command, + get_state_summary=get_state_summary, + run_id=run_id, + ) + except asyncio.CancelledError: + self._clean_up_poller() + await self._publish_runs_advise_unsubscribe_async(run_id=run_id) + await self._client.publish_advise_refetch_async( + topic=Topics.RUNS_CURRENT_COMMAND + ) + except Exception as e: + log.error(f"Error within run data manager poller: {e}") + + async def _poll_for_run_id_info( + self, + get_current_command: Callable[[str], Optional[CurrentCommand]], + get_state_summary: Callable[[str], Optional[StateSummary]], + run_id: str, + ): + """Poll the engine store for a specific run's state while the poll is active. + + Args: + get_current_command: Retrieves the engine store's current command. + get_state_summary: Retrieves the engine store's state summary. + run_id: ID of the current run. + """ + while not self._run_data_manager_polling.is_set(): current_command = get_current_command(run_id) current_state_summary = get_state_summary(run_id) @@ -99,24 +140,44 @@ async def _poll_engine_store( self._previous_current_command = current_command if self._previous_state_summary_status != current_state_summary_status: - await self._publish_runs_async(run_id=run_id) + await self._publish_runs_advise_refetch_async(run_id=run_id) self._previous_state_summary_status = current_state_summary_status - await asyncio.sleep(1) + await asyncio.sleep(POLL_INTERVAL) async def _publish_current_command( self, ) -> None: """Publishes the equivalent of GET /runs/:runId/commands?cursor=null&pageLength=1.""" - await self._client.publish_async(topic=Topics.RUNS_CURRENT_COMMAND) + await self._client.publish_advise_refetch_async( + topic=Topics.RUNS_CURRENT_COMMAND + ) + + async def _publish_runs_advise_refetch_async(self, run_id: str) -> None: + """Asynchronously publishes the equivalent of GET /runs and GET /runs/:runId via a refetch message. + + Args: + run_id: ID of the current run. + """ + await self._client.publish_advise_refetch_async(topic=Topics.RUNS) + await self._client.publish_advise_refetch_async(topic=f"{Topics.RUNS}/{run_id}") - async def _publish_runs_async(self, run_id: str) -> None: - """Asynchronously publishes the equivalent of GET /runs and GET /runs/:runId. + async def _publish_runs_advise_unsubscribe_async(self, run_id: str) -> None: + """Asynchronously publishes the equivalent of GET /runs and GET /runs/:runId via an unsubscribe message. Args: run_id: ID of the current run. """ - await self._client.publish_async(topic=Topics.RUNS) - await self._client.publish_async(topic=f"{Topics.RUNS}/{run_id}") + await self._client.publish_advise_unsubscribe_async(topic=Topics.RUNS) + await self._client.publish_advise_unsubscribe_async( + topic=f"{Topics.RUNS}/{run_id}" + ) + + def _clean_up_poller(self) -> None: + """Cleans up the runs data manager poller.""" + self._poller = None + self._run_data_manager_polling.clear() + self._previous_current_command = None + self._previous_state_summary_status = None _runs_publisher_accessor: AppStateAccessor[RunsPublisher] = AppStateAccessor[ diff --git a/robot-server/tests/runs/router/test_commands_router.py b/robot-server/tests/runs/router/test_commands_router.py index 10819fcac9a..cc06ddd621f 100644 --- a/robot-server/tests/runs/router/test_commands_router.py +++ b/robot-server/tests/runs/router/test_commands_router.py @@ -249,6 +249,24 @@ async def test_get_run_commands( decoy: Decoy, mock_run_data_manager: RunDataManager ) -> None: """It should return a list of all commands in a run.""" + long_note = pe_commands.CommandNote( + noteKind="warning", + shortMessage="this is a warning.", + longMessage=""" + hello, friends. I bring a warning.... + + + + FROM THE FUTURE! + """, + source="test", + ) + unenumed_note = pe_commands.CommandNote( + noteKind="lahsdlasd", + shortMessage="Oh no", + longMessage="its a notekind not in the enum", + source="test2", + ) command = pe_commands.WaitForResume( id="command-id", key="command-key", @@ -264,6 +282,7 @@ async def test_get_run_commands( createdAt=datetime(year=2024, month=4, day=4), detail="Things are not looking good.", ), + notes=[long_note, unenumed_note], ) decoy.when(mock_run_data_manager.get_current_command("run-id")).then_return( @@ -306,6 +325,7 @@ async def test_get_run_commands( createdAt=datetime(year=2024, month=4, day=4), detail="Things are not looking good.", ), + notes=[long_note, unenumed_note], ) ] assert result.content.meta == MultiBodyMeta(cursor=1, totalLength=3) diff --git a/robot-server/tests/runs/test_run_store.py b/robot-server/tests/runs/test_run_store.py index b807cbf1e18..8c696426c76 100644 --- a/robot-server/tests/runs/test_run_store.py +++ b/robot-server/tests/runs/test_run_store.py @@ -5,6 +5,7 @@ import pytest from decoy import Decoy from sqlalchemy.engine import Engine +from unittest import mock from opentrons_shared_data.pipette.dev_types import PipetteNameType @@ -162,6 +163,7 @@ def test_update_run_state( subject: RunStore, state_summary: StateSummary, protocol_commands: List[pe_commands.Command], + mock_runs_publisher: mock.Mock, ) -> None: """It should be able to update a run state to the store.""" action = RunAction( @@ -197,6 +199,9 @@ def test_update_run_state( ) assert run_summary_result == state_summary assert commands_result.commands == protocol_commands + mock_runs_publisher.publish_runs_advise_refetch.assert_called_once_with( + run_id="run-id" + ) def test_update_state_run_not_found( @@ -372,7 +377,7 @@ def test_get_all_runs( assert result == expected_result -def test_remove_run(subject: RunStore) -> None: +def test_remove_run(subject: RunStore, mock_runs_publisher: mock.Mock) -> None: """It can remove a previously stored run entry.""" action = RunAction( actionType=RunActionType.PLAY, @@ -389,6 +394,9 @@ def test_remove_run(subject: RunStore) -> None: subject.remove(run_id="run-id") assert subject.get_all(length=20) == [] + mock_runs_publisher.publish_runs_advise_unsubscribe.assert_called_once_with( + run_id="run-id" + ) def test_remove_run_missing_id(subject: RunStore) -> None: @@ -409,7 +417,9 @@ def test_insert_actions_no_run(subject: RunStore) -> None: subject.insert_action(run_id="run-id-996", action=action) -def test_get_state_summary(subject: RunStore, state_summary: StateSummary) -> None: +def test_get_state_summary( + subject: RunStore, state_summary: StateSummary, mock_runs_publisher: mock.Mock +) -> None: """It should be able to get store run data.""" subject.insert( run_id="run-id", @@ -419,6 +429,9 @@ def test_get_state_summary(subject: RunStore, state_summary: StateSummary) -> No subject.update_run_state(run_id="run-id", summary=state_summary, commands=[]) result = subject.get_state_summary(run_id="run-id") assert result == state_summary + mock_runs_publisher.publish_runs_advise_refetch.assert_called_once_with( + run_id="run-id" + ) def test_get_state_summary_failure( diff --git a/robot-server/tests/service/json_api/test_response.py b/robot-server/tests/service/json_api/test_response.py index 4424774140a..1429d88b5e0 100644 --- a/robot-server/tests/service/json_api/test_response.py +++ b/robot-server/tests/service/json_api/test_response.py @@ -13,6 +13,7 @@ MultiBody, MultiBodyMeta, NotifyRefetchBody, + NotifyUnsubscribeBody, DeprecatedResponseModel, DeprecatedMultiResponseModel, ) @@ -116,6 +117,10 @@ class ResponseSpec(NamedTuple): }, ), ResponseSpec(subject=NotifyRefetchBody(), expected={"refetchUsingHTTP": True}), + ResponseSpec( + subject=NotifyUnsubscribeBody(), + expected={"unsubscribe": True}, + ), ] diff --git a/shared-data/command/schemas/8.json b/shared-data/command/schemas/8.json index c2eb0a0e2a8..a17be9ee690 100644 --- a/shared-data/command/schemas/8.json +++ b/shared-data/command/schemas/8.json @@ -612,7 +612,7 @@ "frontRightNozzle": { "title": "Frontrightnozzle", "description": "The front right nozzle in your configuration.", - "pattern": "[A-Z][0-100]", + "pattern": "[A-Z]\\d{1,2}", "type": "string" } }, diff --git a/shared-data/command/types/index.ts b/shared-data/command/types/index.ts index d3994fc5337..980eb8fb124 100644 --- a/shared-data/command/types/index.ts +++ b/shared-data/command/types/index.ts @@ -31,7 +31,12 @@ export * from './timing' // NOTE: these key/value pairs will only be present on commands at analysis/run time // they pertain only to the actual execution status of a command on hardware, as opposed to // the command's identity and parameters which can be known prior to runtime - +export interface CommandNote { + noteKind: 'warning' | 'information' | string + shortMessage: string + longMessage: string + source: string +} export type CommandStatus = 'queued' | 'running' | 'succeeded' | 'failed' export interface CommonCommandRunTimeInfo { key?: string @@ -42,6 +47,7 @@ export interface CommonCommandRunTimeInfo { startedAt: string | null completedAt: string | null intent?: 'protocol' | 'setup' + notes?: CommandNote[] | null } export interface CommonCommandCreateInfo { key?: string diff --git a/shared-data/js/__tests__/pipettes.test.ts b/shared-data/js/__tests__/pipettes.test.ts index c5f3e4ddd4b..0fe60334c3f 100644 --- a/shared-data/js/__tests__/pipettes.test.ts +++ b/shared-data/js/__tests__/pipettes.test.ts @@ -1,6 +1,11 @@ // tests for pipette info accessors in `shared-data/js/pipettes.js` import { describe, expect, it } from 'vitest' -import { getPipetteNameSpecs, getPipetteModelSpecs } from '../pipettes' +import { + getPipetteSpecsV2, + getPipetteNameSpecs, + getPipetteModelSpecs, +} from '../pipettes' +import type { PipetteV2LiquidSpecs, PipetteV2Specs } from '../types' const PIPETTE_NAMES = [ 'p10_single', @@ -56,4 +61,181 @@ describe('pipette data accessors', () => { expect(getPipetteModelSpecs(model)).toMatchSnapshot()) ) }) + + describe('getPipetteSpecsV2', () => { + it('returns the correct info for p1000_single_flex', () => { + const mockP1000Specs = { + $otSharedSchema: '#/pipette/schemas/2/pipetteGeometrySchema.json', + availableSensors: { + sensors: ['pressure', 'capacitive', 'environment'], + capacitive: { count: 1 }, + environment: { count: 1 }, + pressure: { count: 1 }, + }, + backCompatNames: [], + backlashDistance: 0.1, + channels: 1, + displayCategory: 'FLEX', + displayName: 'Flex 1-Channel 1000 μL', + dropTipConfigurations: { plungerEject: { current: 1, speed: 10 } }, + liquids: { + default: { + $otSharedSchema: + '#/pipette/schemas/2/pipetteLiquidPropertiesSchema.json', + defaultTipOverlapDictionary: { + default: 10.5, + 'opentrons/opentrons_flex_96_tiprack_1000ul/1': 10.5, + 'opentrons/opentrons_flex_96_tiprack_200ul/1': 10.5, + 'opentrons/opentrons_flex_96_tiprack_50ul/1': 10.5, + }, + defaultTipracks: [ + 'opentrons/opentrons_flex_96_tiprack_1000ul/1', + 'opentrons/opentrons_flex_96_tiprack_200ul/1', + 'opentrons/opentrons_flex_96_tiprack_50ul/1', + ], + minVolume: 5, + maxVolume: 1000, + supportedTips: expect.anything(), + }, + }, + model: 'p1000', + nozzleMap: expect.anything(), + pathTo3D: + 'pipette/definitions/2/geometry/single_channel/p1000/placeholder.gltf', + pickUpTipConfigurations: { + pressFit: { + speedByTipCount: expect.anything(), + presses: 1, + increment: 0, + distanceByTipCount: expect.anything(), + currentByTipCount: expect.anything(), + }, + }, + partialTipConfigurations: { + availableConfigurations: null, + partialTipSupported: false, + }, + plungerHomingConfigurations: { current: 1, speed: 30 }, + plungerMotorConfigurations: { idle: 0.3, run: 1 }, + plungerPositionsConfigurations: { + default: { blowout: 76.5, bottom: 71.5, drop: 90.5, top: 0.5 }, + }, + quirks: [], + shaftDiameter: 4.5, + shaftULperMM: 15.904, + nozzleOffset: [-8, -22, -259.15], + orderedColumns: expect.anything(), + orderedRows: expect.anything(), + pipetteBoundingBoxOffsets: { + backLeftCorner: [-8, -22, -259.15], + frontRightCorner: [-8, -22, -259.15], + }, + } as PipetteV2Specs + expect(getPipetteSpecsV2('p1000_single_flex')).toStrictEqual( + mockP1000Specs + ) + }) + }) + it('returns the correct liquid info for a p50 pipette with default and lowVolume', () => { + const tiprack50uL = 'opentrons/opentrons_flex_96_tiprack_50ul/1' + const mockLiquidDefault = { + $otSharedSchema: '#/pipette/schemas/2/pipetteLiquidPropertiesSchema.json', + defaultTipOverlapDictionary: { + default: 10.5, + [tiprack50uL]: 10.5, + }, + defaultTipracks: [tiprack50uL], + maxVolume: 50, + minVolume: 5, + supportedTips: { + t50: { + aspirate: { + default: { + 1: expect.anything(), + }, + }, + defaultAspirateFlowRate: { + default: 35, + valuesByApiLevel: { + '2.14': 35, + }, + }, + defaultBlowOutFlowRate: { + default: 57, + valuesByApiLevel: { + '2.14': 57, + }, + }, + defaultDispenseFlowRate: { + default: 57, + valuesByApiLevel: { + '2.14': 57, + }, + }, + defaultFlowAcceleration: 1200, + defaultPushOutVolume: 2, + defaultReturnTipHeight: 0.71, + defaultTipLength: 57.9, + dispense: { + default: { + 1: expect.anything(), + }, + }, + }, + }, + } as PipetteV2LiquidSpecs + const mockLiquidLowVolume = { + $otSharedSchema: '#/pipette/schemas/2/pipetteLiquidPropertiesSchema.json', + defaultTipOverlapDictionary: { + default: 10.5, + [tiprack50uL]: 10.5, + }, + defaultTipracks: [tiprack50uL], + maxVolume: 30, + minVolume: 1, + supportedTips: { + t50: { + aspirate: { + default: { + 1: expect.anything(), + }, + }, + defaultAspirateFlowRate: { + default: 35, + valuesByApiLevel: { + 2.14: 35, + }, + }, + defaultBlowOutFlowRate: { + default: 57, + valuesByApiLevel: { + 2.14: 57, + }, + }, + defaultDispenseFlowRate: { + default: 57, + valuesByApiLevel: { + 2.14: 57, + }, + }, + defaultFlowAcceleration: 1200, + defaultPushOutVolume: 7, + defaultReturnTipHeight: 0.71, + defaultTipLength: 57.9, + dispense: { + default: { + 1: expect.anything(), + }, + }, + }, + }, + } as PipetteV2LiquidSpecs + const mockLiquids: Record = { + default: mockLiquidDefault, + lowVolumeDefault: mockLiquidLowVolume, + } + expect(getPipetteSpecsV2('p50_single_v3.5')?.liquids).toStrictEqual( + mockLiquids + ) + }) }) diff --git a/shared-data/js/helpers/getSimplestFlexDeckConfig.ts b/shared-data/js/helpers/getSimplestFlexDeckConfig.ts index d8a16033050..e4017199156 100644 --- a/shared-data/js/helpers/getSimplestFlexDeckConfig.ts +++ b/shared-data/js/helpers/getSimplestFlexDeckConfig.ts @@ -80,11 +80,11 @@ export function getSimplestDeckConfigForProtocol( ({ cutoutId }) => cutoutId === cutoutIdForAddressableArea ) const previousRequiredAAs = acc[accIndex]?.requiredAddressableAreas - const allNextRequiredAddressableAreas = previousRequiredAAs.includes( - addressableArea - ) - ? previousRequiredAAs - : [...previousRequiredAAs, addressableArea] + const allNextRequiredAddressableAreas = + previousRequiredAAs != null && + previousRequiredAAs.includes(addressableArea) + ? previousRequiredAAs + : [...previousRequiredAAs, addressableArea] const nextCompatibleCutoutFixture = getSimplestFixtureForAddressableAreas( cutoutIdForAddressableArea, allNextRequiredAddressableAreas, diff --git a/shared-data/js/pipettes.ts b/shared-data/js/pipettes.ts index 12bc00a6a08..5a9fc1a67c9 100644 --- a/shared-data/js/pipettes.ts +++ b/shared-data/js/pipettes.ts @@ -1,9 +1,38 @@ import pipetteNameSpecs from '../pipette/definitions/1/pipetteNameSpecs.json' import pipetteModelSpecs from '../pipette/definitions/1/pipetteModelSpecs.json' import { OT3_PIPETTES } from './constants' +import type { + PipetteV2Specs, + PipetteV2GeneralSpecs, + PipetteV2GeometrySpecs, + PipetteV2LiquidSpecs, + PipetteNameSpecs, + PipetteModelSpecs, +} from './types' -import type { PipetteNameSpecs, PipetteModelSpecs } from './types' +type GeneralGeometricModules = PipetteV2GeneralSpecs | PipetteV2GeometrySpecs +interface GeneralGeometricSpecs { + default: GeneralGeometricModules +} +interface LiquidSpecs { + default: PipetteV2LiquidSpecs +} + +const generalGeometric: Record< + string, + GeneralGeometricSpecs +> = import.meta.glob('../pipette/definitions/2/*/*/*/*.json', { eager: true }) + +const liquid: Record = import.meta.glob( + '../pipette/definitions/2/liquid/*/*/*/*.json', + { + eager: true, + } +) +type PipChannelString = 'single' | 'multi' | '96' +type Channels = 'eight_channel' | 'single_channel' | 'ninety_six_channel' +type Gen = 'gen1' | 'gen2' | 'gen3' | 'flex' type SortableProps = 'maxVolume' | 'channels' // TODO(mc, 2021-04-30): use these types, pulled directly from the JSON, @@ -89,3 +118,115 @@ export const getIncompatiblePipetteNames = ( } export * from '../pipette/fixtures/name' + +const getChannelsFromString = ( + pipChannelString: PipChannelString +): Channels | null => { + switch (pipChannelString) { + case 'single': { + return 'single_channel' + } + case 'multi': { + return 'eight_channel' + } + case '96': { + return 'ninety_six_channel' + } + default: { + console.error(`invalid number of channels from ${pipChannelString}`) + return null + } + } +} +const getVersionFromGen = (gen: Gen): string | null => { + switch (gen) { + case 'gen1': { + return '1_0' + } + case 'gen2': { + return '2_0' + } + case 'gen3': + case 'flex': { + return '3_0' + } + default: { + return null + } + } +} + +const V2_DEFINITION_TYPES = ['general', 'geometry'] + +/* takes in pipetteName such as 'p300_single' or 'p300_single_gen1' +or PipetteModel such as 'p300_single_v1.3' and converts it to channels, +model, and version in order to return the correct pipette schema v2 json files. +**/ +export const getPipetteSpecsV2 = ( + name: PipetteName | PipetteModel +): PipetteV2Specs | null => { + const nameSplit = name.split('_') + const pipetteModel = nameSplit[0] // ex: p300 + const channels = getChannelsFromString(nameSplit[1] as PipChannelString) // ex: single -> single_channel + const gen = getVersionFromGen(nameSplit[2] as Gen) + + let version: string + // the first 2 conditions are to accommodate version from the pipetteName + if (nameSplit.length === 2) { + version = '1_0' + } else if (gen != null) { + version = gen // ex: gen1 -> 1_0 + // the 'else' is to accommodate the exact version if PipetteModel was added + } else { + const versionNumber = nameSplit[2].split('v')[1] + if (versionNumber.includes('.')) { + version = versionNumber.replace('.', '_') // ex: 1.0 -> 1_0 + } else { + version = `${versionNumber}_0` // ex: 1 -> 1_0 + } + } + + const generalGeometricMatchingJsons = Object.entries(generalGeometric).reduce( + (genericGeometricModules: GeneralGeometricModules[], [path, module]) => { + V2_DEFINITION_TYPES.forEach(type => { + if ( + `../pipette/definitions/2/${type}/${channels}/${pipetteModel}/${version}.json` === + path + ) { + genericGeometricModules.push(module.default) + } + }) + return genericGeometricModules + }, + [] + ) + + const liquidTypes: string[] = [] + const liquidMatchingJsons: { + liquids: Record + } = { liquids: {} } + + Object.entries(liquid).forEach(([path, module]) => { + const type = path.split('/')[7] + // dynamically check the different liquid types and store unique types + // into an array to parse through + if (!liquidTypes.includes(type)) { + liquidTypes.push(type) + } + if ( + `../pipette/definitions/2/liquid/${channels}/${pipetteModel}/${type}/${version}.json` === + path + ) { + const index = liquidTypes.indexOf(type) + const newKeyName = index !== -1 ? liquidTypes[index] : path + liquidMatchingJsons.liquids[newKeyName] = module.default + } + }) + + const pipetteV2Specs: PipetteV2Specs = { + ...Object.assign({}, ...generalGeometricMatchingJsons), + ...liquidMatchingJsons, + } + + return pipetteV2Specs +} diff --git a/shared-data/js/types.ts b/shared-data/js/types.ts index 82fd65fb87d..8c26c58411e 100644 --- a/shared-data/js/types.ts +++ b/shared-data/js/types.ts @@ -394,6 +394,124 @@ export interface FlowRateSpec { max: number } +export interface PipetteV2GeneralSpecs { + displayName: string + model: string + displayCategory: PipetteDisplayCategory + pickUpTipConfigurations: { + pressFit: { + speedByTipCount: Record + presses: number + increment: number + distanceByTipCount: Record + currentByTipCount: Record + } + } + dropTipConfigurations: { + plungerEject: { + current: number + speed: number + } + } + plungerMotorConfigurations: { + idle: number + run: number + } + plungerPositionsConfigurations: { + default: { + top: number + bottom: number + blowout: number + drop: number + } + } + availableSensors: { + sensors: string[] + capacitive?: { count: number } + environment?: { count: number } + pressure?: { count: number } + } + partialTipConfigurations: { + partialTipSupported: boolean + availableConfigurations: number[] | null + } + channels: number + shaftDiameter: number + shaftULperMM: number + backCompatNames: string[] + backlashDistance: number + quirks: string[] + plungerHomingConfigurations: { + current: number + speed: number + } +} + +interface NozzleInfo { + key: string + orderedNozzles: string[] +} +export interface PipetteV2GeometrySpecs { + nozzleOffset: number[] + pipetteBoundingBoxOffsets: { + backLeftCorner: number[] + frontRightCorner: number[] + } + pathTo3D: string + orderedRows: Record + orderedColumns: Record + nozzleMap: Record +} + +type TipData = [number, number, number] +interface SupportedTips { + [tipType: string]: { + aspirate: { + default: { + 1: TipData + } + } + defaultAspirateFlowRate: { + default: number + valuesByApiLevel: Record + } + defaultBlowOutFlowRate: { + default: number + valuesByApiLevel: Record + } + defaultDispenseFlowRate: { + default: number + valuesByApiLevel: Record + } + defaultFlowAcceleration: number + defaultPushOutVolume: number + defaultReturnTipHeight: number + defaultTipLength: number + dispense: { + default: { + 1: TipData + } + } + } +} + +export interface PipetteV2LiquidSpecs { + $otSharedSchema: string + supportedTips: SupportedTips + defaultTipOverlapDictionary: Record + maxVolume: number + minVolume: number + defaultTipracks: string[] +} + +export type GenericAndGeometrySpecs = PipetteV2GeneralSpecs & + PipetteV2GeometrySpecs + +export interface PipetteV2Specs extends GenericAndGeometrySpecs { + $otSharedSchema: string + liquids: Record +} + export interface PipetteNameSpecs { name: string displayName: string diff --git a/step-generation/src/commandCreators/atomic/disengageMagnet.ts b/step-generation/src/commandCreators/atomic/disengageMagnet.ts index 567fb6d2c29..4a4b56f9587 100644 --- a/step-generation/src/commandCreators/atomic/disengageMagnet.ts +++ b/step-generation/src/commandCreators/atomic/disengageMagnet.ts @@ -13,7 +13,7 @@ export const disengageMagnet: CommandCreator = ( const { module: moduleId } = args const commandType = 'magneticModule/disengage' - if (module === null) { + if (moduleId === null) { return { errors: [errorCreators.missingModuleError()], } diff --git a/step-generation/src/commandCreators/atomic/engageMagnet.ts b/step-generation/src/commandCreators/atomic/engageMagnet.ts index 6d1a0070d14..da5f8af11b7 100644 --- a/step-generation/src/commandCreators/atomic/engageMagnet.ts +++ b/step-generation/src/commandCreators/atomic/engageMagnet.ts @@ -13,7 +13,7 @@ export const engageMagnet: CommandCreator = ( const { module: moduleId, engageHeight } = args const commandType = 'magneticModule/engage' - if (module === null) { + if (moduleId === null) { return { errors: [errorCreators.missingModuleError()], } diff --git a/step-generation/src/commandCreators/atomic/heaterShakerSetTargetShakeSpeed.ts b/step-generation/src/commandCreators/atomic/heaterShakerSetTargetShakeSpeed.ts index 58c9af666b0..24721a2967d 100644 --- a/step-generation/src/commandCreators/atomic/heaterShakerSetTargetShakeSpeed.ts +++ b/step-generation/src/commandCreators/atomic/heaterShakerSetTargetShakeSpeed.ts @@ -10,7 +10,7 @@ export const heaterShakerSetTargetShakeSpeed: CommandCreator ) => { const { moduleId, rpm } = args - if (module === null) { + if (moduleId === null) { return { errors: [errorCreators.missingModuleError()], } diff --git a/step-generation/src/robotStateSelectors.ts b/step-generation/src/robotStateSelectors.ts index 3b13aecf86d..0fcf26e6675 100644 --- a/step-generation/src/robotStateSelectors.ts +++ b/step-generation/src/robotStateSelectors.ts @@ -1,7 +1,6 @@ import assert from 'assert' // TODO: Ian 2019-04-18 move orderWells somewhere more general -- shared-data util? import min from 'lodash/min' -import sortBy from 'lodash/sortBy' import { getTiprackVolume, THERMOCYCLER_MODULE_TYPE, @@ -10,19 +9,40 @@ import { COLUMN, ALL, } from '@opentrons/shared-data' +import { COLUMN_4_SLOTS } from './constants' import type { InvariantContext, ModuleTemporalProperties, RobotState, ThermocyclerModuleState, -} from './' +} from './types' + export function sortLabwareBySlot( labwareState: RobotState['labware'] ): string[] { - return sortBy(Object.keys(labwareState), (id: string) => - parseInt(labwareState[id].slot) + const sortedLabware = Object.keys(labwareState).sort( + (idA: string, idB: string) => { + const slotA = parseInt(labwareState[idA].slot) + const slotB = parseInt(labwareState[idB].slot) + if ( + COLUMN_4_SLOTS.includes(labwareState[idA].slot) && + COLUMN_4_SLOTS.includes(labwareState[idB].slot) + ) { + return idA.localeCompare(idB) + } + if (COLUMN_4_SLOTS.includes(labwareState[idA].slot)) { + return 1 + } + if (COLUMN_4_SLOTS.includes(labwareState[idB].slot)) { + return -1 + } + return slotA - slotB + } ) + + return sortedLabware } + export function _getNextTip(args: { pipetteId: string tiprackId: string