From 7f5a687e2185754c7c74763e50a9396e6a48182c Mon Sep 17 00:00:00 2001 From: Jamey Huffnagle Date: Mon, 11 Mar 2024 13:03:57 -0400 Subject: [PATCH 1/9] fix(app): properly manage the ipcRenderer notify event emitter (#14621) Closes RQA-2459 Do not instantiate new notification ipcRenderer event listeners for each component that intends to subscribe to the notification server. Instead re-use the same ipcRenderer listener. This not only reduces the number of emitters created, but fixes a bug in which ipcRenderer listeners were not properly disposed. --- app/src/redux/shell/remote.ts | 56 ++++++++++++++----- app/src/redux/shell/types.ts | 20 +++---- .../__tests__/useNotifyService.test.ts | 30 +++++++--- app/src/resources/useNotifyService.ts | 18 +++++- 4 files changed, 90 insertions(+), 34 deletions(-) diff --git a/app/src/redux/shell/remote.ts b/app/src/redux/shell/remote.ts index 18af0af5a6e..7ddc6235482 100644 --- a/app/src/redux/shell/remote.ts +++ b/app/src/redux/shell/remote.ts @@ -37,20 +37,50 @@ export function appShellRequestor( return remote.ipcRenderer.invoke('usb:request', configProxy) } -export function appShellListener( - hostname: string | null, - topic: NotifyTopic, +interface CallbackStore { + [hostname: string]: { + [topic in NotifyTopic]: Array<(data: NotifyResponseData) => void> + } +} +const callbackStore: CallbackStore = {} + +interface AppShellListener { + hostname: string + topic: NotifyTopic callback: (data: NotifyResponseData) => void -): void { - remote.ipcRenderer.on( - 'notify', - (_, shellHostname, shellTopic, shellMessage) => { - if ( - hostname === shellHostname && - (topic === shellTopic || shellTopic === 'ALL_TOPICS') - ) { - callback(shellMessage) + isDismounting?: boolean +} +export function appShellListener({ + hostname, + topic, + callback, + isDismounting = false, +}: AppShellListener): CallbackStore { + if (isDismounting) { + const callbacks = callbackStore[hostname]?.[topic] + if (callbacks != null) { + callbackStore[hostname][topic] = callbacks.filter(cb => cb !== callback) + if (!callbackStore[hostname][topic].length) { + // eslint-disable-next-line @typescript-eslint/no-dynamic-delete + delete callbackStore[hostname][topic] + if (!Object.keys(callbackStore[hostname]).length) { + // eslint-disable-next-line @typescript-eslint/no-dynamic-delete + delete callbackStore[hostname] + } } } - ) + } else { + callbackStore[hostname] = callbackStore[hostname] ?? {} + callbackStore[hostname][topic] ??= [] + callbackStore[hostname][topic].push(callback) + } + return callbackStore } + +// Instantiate the notify listener at runtime. +remote.ipcRenderer.on( + 'notify', + (_, shellHostname, shellTopic, shellMessage) => { + callbackStore[shellHostname]?.[shellTopic]?.forEach(cb => cb(shellMessage)) + } +) diff --git a/app/src/redux/shell/types.ts b/app/src/redux/shell/types.ts index 379d22bd892..fe221d7d3f2 100644 --- a/app/src/redux/shell/types.ts +++ b/app/src/redux/shell/types.ts @@ -6,19 +6,19 @@ export interface Remote { ipcRenderer: { invoke: (channel: string, ...args: unknown[]) => Promise send: (channel: string, ...args: unknown[]) => void - on: ( - channel: string, - listener: ( - event: IpcMainEvent, - hostname: string, - topic: NotifyTopic, - message: NotifyResponseData | NotifyNetworkError, - ...args: unknown[] - ) => void - ) => void + on: (channel: string, listener: IpcListener) => void + off: (channel: string, listener: IpcListener) => void } } +export type IpcListener = ( + event: IpcMainEvent, + hostname: string, + topic: NotifyTopic, + message: NotifyResponseData | NotifyNetworkError, + ...args: unknown[] +) => void + interface NotifyRefetchData { refetchUsingHTTP: boolean statusCode: never diff --git a/app/src/resources/__tests__/useNotifyService.test.ts b/app/src/resources/__tests__/useNotifyService.test.ts index 6946f8f8c17..f5ead537b15 100644 --- a/app/src/resources/__tests__/useNotifyService.test.ts +++ b/app/src/resources/__tests__/useNotifyService.test.ts @@ -74,7 +74,7 @@ describe('useNotifyService', () => { expect(mockDispatch).not.toHaveBeenCalledWith( notifyUnsubscribeAction(MOCK_HOST_CONFIG.hostname, MOCK_TOPIC) ) - expect(appShellListener).toHaveBeenCalled() + expect(mockAppShellListener).toHaveBeenCalled() }) it('should trigger an unsubscribe action on dismount', () => { @@ -100,7 +100,7 @@ describe('useNotifyService', () => { } as any) ) expect(mockHTTPRefetch).toHaveBeenCalled() - expect(appShellListener).not.toHaveBeenCalled() + expect(mockAppShellListener).not.toHaveBeenCalled() expect(mockDispatch).not.toHaveBeenCalled() }) @@ -113,7 +113,7 @@ describe('useNotifyService', () => { } as any) ) expect(mockHTTPRefetch).toHaveBeenCalled() - expect(appShellListener).not.toHaveBeenCalled() + expect(mockAppShellListener).not.toHaveBeenCalled() expect(mockDispatch).not.toHaveBeenCalled() }) @@ -126,7 +126,7 @@ describe('useNotifyService', () => { } as any) ) expect(mockHTTPRefetch).toHaveBeenCalled() - expect(appShellListener).not.toHaveBeenCalled() + expect(mockAppShellListener).not.toHaveBeenCalled() expect(mockDispatch).not.toHaveBeenCalled() }) @@ -144,8 +144,9 @@ describe('useNotifyService', () => { }) it('should return set HTTP refetch to always and fire an analytics reporting event if the connection was refused', () => { - mockAppShellListener.mockImplementation((_: any, __: any, mockCb: any) => { - mockCb('ECONNREFUSED') + mockAppShellListener.mockImplementation(function ({ callback }): any { + // eslint-disable-next-line n/no-callback-literal + callback('ECONNREFUSED') }) const { rerender } = renderHook(() => useNotifyService({ @@ -160,8 +161,9 @@ describe('useNotifyService', () => { }) it('should trigger a single HTTP refetch if the refetch flag was returned', () => { - mockAppShellListener.mockImplementation((_: any, __: any, mockCb: any) => { - mockCb({ refetchUsingHTTP: true }) + mockAppShellListener.mockImplementation(function ({ callback }): any { + // eslint-disable-next-line n/no-callback-literal + callback('ECONNREFUSED') }) const { rerender } = renderHook(() => useNotifyService({ @@ -173,4 +175,16 @@ describe('useNotifyService', () => { rerender() expect(mockHTTPRefetch).toHaveBeenCalledWith('once') }) + + it('should clean up the listener on dismount', () => { + const { unmount } = renderHook(() => + useNotifyService({ + topic: MOCK_TOPIC, + setRefetchUsingHTTP: mockHTTPRefetch, + options: MOCK_OPTIONS, + }) + ) + unmount() + expect(mockAppShellListener).toHaveBeenCalled() + }) }) diff --git a/app/src/resources/useNotifyService.ts b/app/src/resources/useNotifyService.ts index 3accf0b8082..b4b208ee8e9 100644 --- a/app/src/resources/useNotifyService.ts +++ b/app/src/resources/useNotifyService.ts @@ -55,14 +55,26 @@ export function useNotifyService({ if (shouldUseNotifications) { // Always fetch on initial mount. setRefetchUsingHTTP('once') - appShellListener(hostname, topic, onDataEvent) + appShellListener({ + hostname, + topic, + callback: onDataEvent, + }) dispatch(notifySubscribeAction(hostname, topic)) hasUsedNotifyService.current = true } else setRefetchUsingHTTP('always') return () => { - if (hasUsedNotifyService.current && hostname != null) { - dispatch(notifyUnsubscribeAction(hostname, topic)) + if (hasUsedNotifyService.current) { + if (hostname != null) { + dispatch(notifyUnsubscribeAction(hostname, topic)) + } + appShellListener({ + hostname: hostname as string, + topic, + callback: onDataEvent, + isDismounting: true, + }) } } }, [topic, host, shouldUseNotifications]) From 5090243ef23746e8cfb713ffee66680065eb2e71 Mon Sep 17 00:00:00 2001 From: Sanniti Pimpley Date: Mon, 11 Mar 2024 13:59:48 -0400 Subject: [PATCH 2/9] feat(api): remove z safety margin from pipette movement check (#14613) Closes RESC-216 --- .../protocol_api/core/engine/deck_conflict.py | 5 +--- .../protocol_api/instrument_context.py | 2 ++ .../test_pipette_movement_deck_conflicts.py | 30 +++++++++++++++++++ 3 files changed, 33 insertions(+), 4 deletions(-) diff --git a/api/src/opentrons/protocol_api/core/engine/deck_conflict.py b/api/src/opentrons/protocol_api/core/engine/deck_conflict.py index dc290fe01ae..f242fc87836 100644 --- a/api/src/opentrons/protocol_api/core/engine/deck_conflict.py +++ b/api/src/opentrons/protocol_api/core/engine/deck_conflict.py @@ -79,9 +79,6 @@ def __init__(self, message: str) -> None: x=A12_column_back_right_bound.x - _NOZZLE_PITCH * 11, y=506.2 ) -# Arbitrary safety margin in z-direction -Z_SAFETY_MARGIN = 10 - _FLEX_TC_LID_BACK_LEFT_PT = Point( x=FLEX_TC_LID_COLLISION_ZONE["back_left"]["x"], y=FLEX_TC_LID_COLLISION_ZONE["back_left"]["y"], @@ -333,7 +330,7 @@ def _slot_has_potential_colliding_object( slot_highest_z = engine_state.geometry.get_highest_z_in_slot( StagingSlotLocation(slotName=surrounding_slot) ) - return slot_highest_z + Z_SAFETY_MARGIN > pipette_bounds[0].z + return slot_highest_z >= pipette_bounds[0].z return False diff --git a/api/src/opentrons/protocol_api/instrument_context.py b/api/src/opentrons/protocol_api/instrument_context.py index 45b7d385684..03b843e4ffa 100644 --- a/api/src/opentrons/protocol_api/instrument_context.py +++ b/api/src/opentrons/protocol_api/instrument_context.py @@ -1103,6 +1103,7 @@ def home_plunger(self) -> InstrumentContext: self._core.home_plunger() return self + # TODO (spp, 2024-03-08): verify if ok to & change source & dest types to AdvancedLiquidHandling @publisher.publish(command=cmds.distribute) @requires_version(2, 0) def distribute( @@ -1142,6 +1143,7 @@ def distribute( return self.transfer(volume, source, dest, **kwargs) + # TODO (spp, 2024-03-08): verify if ok to & change source & dest types to AdvancedLiquidHandling @publisher.publish(command=cmds.consolidate) @requires_version(2, 0) def consolidate( diff --git a/api/tests/opentrons/protocol_api_integration/test_pipette_movement_deck_conflicts.py b/api/tests/opentrons/protocol_api_integration/test_pipette_movement_deck_conflicts.py index 2ad7b63615a..33e92086edb 100644 --- a/api/tests/opentrons/protocol_api_integration/test_pipette_movement_deck_conflicts.py +++ b/api/tests/opentrons/protocol_api_integration/test_pipette_movement_deck_conflicts.py @@ -99,6 +99,36 @@ def test_deck_conflicts_for_96_ch_a12_column_configuration() -> None: instrument.dispense(50, accessible_plate.wells_by_name()["A1"]) +@pytest.mark.ot3_only +def test_close_shave_deck_conflicts_for_96_ch_a12_column_configuration() -> None: + """Shouldn't raise errors for "almost collision"s.""" + protocol_context = simulate.get_protocol_api(version="2.16", robot_type="Flex") + res12 = protocol_context.load_labware("nest_12_reservoir_15ml", "C3") + + # Mag block and tiprack adapter are very close to the destination reservoir labware + protocol_context.load_module("magneticBlockV1", "D2") + protocol_context.load_labware( + "opentrons_flex_96_tiprack_200ul", + "B3", + adapter="opentrons_flex_96_tiprack_adapter", + ) + tiprack_8 = protocol_context.load_labware("opentrons_flex_96_tiprack_200ul", "B2") + hs = protocol_context.load_module("heaterShakerModuleV1", "D1") + hs_adapter = hs.load_adapter("opentrons_96_deep_well_adapter") + deepwell = hs_adapter.load_labware("nest_96_wellplate_2ml_deep") + protocol_context.load_trash_bin("A3") + p1000_96 = protocol_context.load_instrument("flex_96channel_1000") + p1000_96.configure_nozzle_layout(style=COLUMN, start="A12", tip_racks=[tiprack_8]) + + hs.close_labware_latch() # type: ignore[union-attr] + p1000_96.distribute( + 15, + res12.wells()[0], + deepwell.rows()[0], + disposal_vol=0, + ) + + @pytest.mark.ot3_only def test_deck_conflicts_for_96_ch_a1_column_configuration() -> None: """It should raise errors for expected deck conflicts.""" From 3dfbb6fa4ddf5de8bcfd0e1fece82b67bcc861e7 Mon Sep 17 00:00:00 2001 From: Max Marrone Date: Mon, 11 Mar 2024 15:16:36 -0400 Subject: [PATCH 3/9] fix(app): Tolerate old tip length calibration records without a `uri` field (#14622) --- .../calibration_storage/ot2/deck_attitude.py | 4 +- .../calibration_storage/ot2/pipette_offset.py | 3 +- .../calibration_storage/ot2/tip_length.py | 49 ++++++++++++------- .../calibration_storage/ot3/deck_attitude.py | 4 +- .../calibration_storage/ot3/module_offset.py | 6 ++- .../calibration_storage/ot3/pipette_offset.py | 3 +- api/src/opentrons/config/robot_configs.py | 2 +- .../test_tip_length_ot2.py | 28 +++++++++++ 8 files changed, 73 insertions(+), 26 deletions(-) diff --git a/api/src/opentrons/calibration_storage/ot2/deck_attitude.py b/api/src/opentrons/calibration_storage/ot2/deck_attitude.py index 3f85ad25c17..8edd2e52662 100644 --- a/api/src/opentrons/calibration_storage/ot2/deck_attitude.py +++ b/api/src/opentrons/calibration_storage/ot2/deck_attitude.py @@ -79,7 +79,7 @@ def get_robot_deck_attitude() -> Optional[v1.DeckCalibrationModel]: pass except (json.JSONDecodeError, ValidationError): log.warning( - "Deck calibration is malformed. Please factory reset your calibrations." + "Deck calibration is malformed. Please factory reset your calibrations.", + exc_info=True, ) - pass return None diff --git a/api/src/opentrons/calibration_storage/ot2/pipette_offset.py b/api/src/opentrons/calibration_storage/ot2/pipette_offset.py index ac09a736b4e..a4175b90545 100644 --- a/api/src/opentrons/calibration_storage/ot2/pipette_offset.py +++ b/api/src/opentrons/calibration_storage/ot2/pipette_offset.py @@ -92,7 +92,8 @@ def get_pipette_offset( return None except (json.JSONDecodeError, ValidationError): log.warning( - f"Malformed calibrations for {pipette_id} on {mount}. Please factory reset your calibrations." + f"Malformed calibrations for {pipette_id} on {mount}. Please factory reset your calibrations.", + exc_info=True, ) # TODO: Delete the bad calibration here maybe? return None diff --git a/api/src/opentrons/calibration_storage/ot2/tip_length.py b/api/src/opentrons/calibration_storage/ot2/tip_length.py index 7aff6ec9515..8b5e5369805 100644 --- a/api/src/opentrons/calibration_storage/ot2/tip_length.py +++ b/api/src/opentrons/calibration_storage/ot2/tip_length.py @@ -37,28 +37,43 @@ def _convert_tip_length_model_to_dict( def tip_lengths_for_pipette( pipette_id: str, ) -> typing.Dict[LabwareUri, v1.TipLengthModel]: - tip_lengths = {} try: tip_length_filepath = config.get_tip_length_cal_path() / f"{pipette_id}.json" all_tip_lengths_for_pipette = io.read_cal_file(tip_length_filepath) - for tiprack_identifier, data in all_tip_lengths_for_pipette.items(): - # We normally key these calibrations by their tip rack URI, - # but older software had them keyed by their tip rack hash. - # Migrate from the old format, if necessary. - if "/" not in tiprack_identifier: - data["definitionHash"] = tiprack_identifier - tiprack_identifier = data.pop("uri") - try: - tip_lengths[LabwareUri(tiprack_identifier)] = v1.TipLengthModel(**data) - except (json.JSONDecodeError, ValidationError): - log.warning( - f"Tip length calibration is malformed for {tiprack_identifier} on {pipette_id}" - ) - pass - return tip_lengths except FileNotFoundError: log.debug(f"Tip length calibrations not found for {pipette_id}") - return tip_lengths + return {} + except json.JSONDecodeError: + log.warning( + f"Tip length calibration is malformed for {pipette_id}", exc_info=True + ) + return {} + + tip_lengths: typing.Dict[LabwareUri, v1.TipLengthModel] = {} + + for tiprack_identifier, data in all_tip_lengths_for_pipette.items(): + # We normally key these calibrations by their tip rack URI, + # but older software had them keyed by their tip rack hash. + # Migrate from the old format, if necessary. + tiprack_identifier_is_uri = "/" in tiprack_identifier + if not tiprack_identifier_is_uri: + data["definitionHash"] = tiprack_identifier + uri = data.pop("uri", None) + if uri is None: + # We don't have a way to migrate old records without a URI, + # so skip over them. + continue + else: + tiprack_identifier = uri + + try: + tip_lengths[LabwareUri(tiprack_identifier)] = v1.TipLengthModel(**data) + except ValidationError: + log.warning( + f"Tip length calibration is malformed for {tiprack_identifier} on {pipette_id}", + exc_info=True, + ) + return tip_lengths def load_tip_length_calibration( diff --git a/api/src/opentrons/calibration_storage/ot3/deck_attitude.py b/api/src/opentrons/calibration_storage/ot3/deck_attitude.py index 8f779e4338a..6187459d461 100644 --- a/api/src/opentrons/calibration_storage/ot3/deck_attitude.py +++ b/api/src/opentrons/calibration_storage/ot3/deck_attitude.py @@ -77,7 +77,7 @@ def get_robot_belt_attitude() -> Optional[v1.BeltCalibrationModel]: pass except (json.JSONDecodeError, ValidationError): log.warning( - "Belt calibration is malformed. Please factory reset your calibrations." + "Belt calibration is malformed. Please factory reset your calibrations.", + exc_info=True, ) - pass return None diff --git a/api/src/opentrons/calibration_storage/ot3/module_offset.py b/api/src/opentrons/calibration_storage/ot3/module_offset.py index 800ab8380e6..b9a030d1208 100644 --- a/api/src/opentrons/calibration_storage/ot3/module_offset.py +++ b/api/src/opentrons/calibration_storage/ot3/module_offset.py @@ -108,7 +108,8 @@ def get_module_offset( return None except (json.JSONDecodeError, ValidationError): log.warning( - f"Malformed calibrations for {module_id} on slot {slot}. Please factory reset your calibrations." + f"Malformed calibrations for {module_id} on slot {slot}. Please factory reset your calibrations.", + exc_info=True, ) return None @@ -130,7 +131,8 @@ def load_all_module_offsets() -> List[v1.ModuleOffsetModel]: ) except (json.JSONDecodeError, ValidationError): log.warning( - f"Malformed module calibrations for {file}. Please factory reset your calibrations." + f"Malformed module calibrations for {file}. Please factory reset your calibrations.", + exc_info=True, ) continue return calibrations diff --git a/api/src/opentrons/calibration_storage/ot3/pipette_offset.py b/api/src/opentrons/calibration_storage/ot3/pipette_offset.py index fcd53bbbf3e..a1e6e1090db 100644 --- a/api/src/opentrons/calibration_storage/ot3/pipette_offset.py +++ b/api/src/opentrons/calibration_storage/ot3/pipette_offset.py @@ -89,6 +89,7 @@ def get_pipette_offset( return None except (json.JSONDecodeError, ValidationError): log.warning( - f"Malformed calibrations for {pipette_id} on {mount}. Please factory reset your calibrations." + f"Malformed calibrations for {pipette_id} on {mount}. Please factory reset your calibrations.", + exc_info=True, ) return None diff --git a/api/src/opentrons/config/robot_configs.py b/api/src/opentrons/config/robot_configs.py index d30109dc697..bcb6d6076da 100755 --- a/api/src/opentrons/config/robot_configs.py +++ b/api/src/opentrons/config/robot_configs.py @@ -148,7 +148,7 @@ def _load_json(filename: Union[str, Path]) -> Dict[str, Any]: log.warning("{0} not found. Loading defaults".format(filename)) res = {} except json.decoder.JSONDecodeError: - log.warning("{0} is corrupt. Loading defaults".format(filename)) + log.warning("{0} is corrupt. Loading defaults".format(filename), exc_info=True) res = {} return cast(Dict[str, Any], res) diff --git a/api/tests/opentrons/calibration_storage/test_tip_length_ot2.py b/api/tests/opentrons/calibration_storage/test_tip_length_ot2.py index 4b63b52d3fc..2d593bda67e 100644 --- a/api/tests/opentrons/calibration_storage/test_tip_length_ot2.py +++ b/api/tests/opentrons/calibration_storage/test_tip_length_ot2.py @@ -113,3 +113,31 @@ def test_delete_all_tip_calibration(starting_calibration_data: Any) -> None: clear_tip_length_calibration() assert tip_lengths_for_pipette("pip1") == {} assert tip_lengths_for_pipette("pip2") == {} + + +def test_uriless_calibrations_are_dropped(ot_config_tempdir: object) -> None: + """Legacy records without a `uri` field should be silently ignored.""" + + data = { + "ed323db6ca1ddf197aeb20667c1a7a91c89cfb2f931f45079d483928da056812": { + "tipLength": 123, + "lastModified": "2021-01-11T00:34:29.291073+00:00", + "source": "user", + "status": {"markedBad": False}, + }, + "130e17bb7b2f0c0472dcc01c1ff6f600ca1a6f9f86a90982df56c4bf43776824": { + "tipLength": 456, + "lastModified": "2021-05-12T22:16:14.249567+00:00", + "source": "user", + "status": {"markedBad": False}, + "uri": "opentrons/opentrons_96_filtertiprack_200ul/1", + }, + } + + io.save_to_file(config.get_tip_length_cal_path(), "pipette1234", data) + result = tip_lengths_for_pipette("pipette1234") + assert len(result) == 1 + assert ( + result[LabwareUri("opentrons/opentrons_96_filtertiprack_200ul/1")].tipLength + == 456 + ) From f99718c0e53b4879838f534f4267a6b45517495e Mon Sep 17 00:00:00 2001 From: Jamey Huffnagle Date: Mon, 11 Mar 2024 15:45:22 -0400 Subject: [PATCH 4/9] fix(app): fix querying /runs/null (#14624) Closes RQA-2486 --- app/src/resources/runs/useNotifyRunQuery.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/src/resources/runs/useNotifyRunQuery.ts b/app/src/resources/runs/useNotifyRunQuery.ts index 1da90ee7a08..2de5e273160 100644 --- a/app/src/resources/runs/useNotifyRunQuery.ts +++ b/app/src/resources/runs/useNotifyRunQuery.ts @@ -21,15 +21,17 @@ export function useNotifyRunQuery( setRefetchUsingHTTP, ] = React.useState(null) + const isEnabled = options.enabled !== false && runId != null + useNotifyService({ topic: `robot-server/runs/${runId}` as NotifyTopic, setRefetchUsingHTTP, - options: { ...options, enabled: options.enabled && runId != null }, + options: { ...options, enabled: isEnabled }, }) const httpResponse = useRunQuery(runId, { ...options, - enabled: options?.enabled !== false && refetchUsingHTTP != null, + enabled: isEnabled && refetchUsingHTTP != null, onSettled: refetchUsingHTTP === 'once' ? () => setRefetchUsingHTTP(null) From 7de6f77003d0d51fbcf4a188a43f74ff69fede83 Mon Sep 17 00:00:00 2001 From: Ed Cormany Date: Wed, 13 Mar 2024 14:35:45 -0400 Subject: [PATCH 5/9] fix(app): capitalize "attach gripper" button in protocol setup (#14617) RQA-2496 This "attach gripper" button text wasn't capitalized. Now it is. --- .../Devices/ProtocolRun/SetupGripperCalibrationItem.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/organisms/Devices/ProtocolRun/SetupGripperCalibrationItem.tsx b/app/src/organisms/Devices/ProtocolRun/SetupGripperCalibrationItem.tsx index 431ecbf5529..255a69f467c 100644 --- a/app/src/organisms/Devices/ProtocolRun/SetupGripperCalibrationItem.tsx +++ b/app/src/organisms/Devices/ProtocolRun/SetupGripperCalibrationItem.tsx @@ -26,7 +26,7 @@ export function SetupGripperCalibrationItem({ gripperData, runId, }: SetupGripperCalibrationItemProps): JSX.Element | null { - const { t } = useTranslation('protocol_setup') + const { t, i18n } = useTranslation('protocol_setup') const [ openWizardFlowType, setOpenWizardFlowType, @@ -47,7 +47,7 @@ export function SetupGripperCalibrationItem({ setOpenWizardFlowType(GRIPPER_FLOW_TYPES.ATTACH) }} > - {t('attach_gripper')} + {i18n.format(t('attach_gripper'), 'capitalize')} ) From b5fa901ce3e10016000171af2019477b7d16aa9c Mon Sep 17 00:00:00 2001 From: Alise Au <20424172+ahiuchingau@users.noreply.github.com> Date: Wed, 6 Mar 2024 16:58:54 -0500 Subject: [PATCH 6/9] fix(api): FLEX fast home collision hangs because of deadlock (#14602) --- api/src/opentrons/hardware_control/ot3api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/src/opentrons/hardware_control/ot3api.py b/api/src/opentrons/hardware_control/ot3api.py index 15de962d442..4099728f64a 100644 --- a/api/src/opentrons/hardware_control/ot3api.py +++ b/api/src/opentrons/hardware_control/ot3api.py @@ -1542,7 +1542,7 @@ async def _home_axis(self, axis: Axis) -> None: self._log.warning( f"Stall on {axis} during fast home, encoder may have missed an overflow" ) - await self.refresh_positions() + await self.refresh_positions(acquire_lock=False) await self._backend.home([axis], self.gantry_load) else: From bd12333a51a2567664896756e92b222fbf41e8e5 Mon Sep 17 00:00:00 2001 From: Alise Au <20424172+ahiuchingau@users.noreply.github.com> Date: Fri, 8 Mar 2024 13:24:07 -0500 Subject: [PATCH 7/9] fix(api): raise error if fast home is stalling (#14609) Closes RQA-2312 We previously swallowed collision errors during fast home move and proceeded to slow home because we used to not handle encoder overflow properly and would mistakenly raise. Now that we have fixed those encoder issues, we should actually raise the error when the motor stalls. --- api/src/opentrons/hardware_control/ot3api.py | 22 ++++++-------------- 1 file changed, 6 insertions(+), 16 deletions(-) diff --git a/api/src/opentrons/hardware_control/ot3api.py b/api/src/opentrons/hardware_control/ot3api.py index 4099728f64a..cf76723c20c 100644 --- a/api/src/opentrons/hardware_control/ot3api.py +++ b/api/src/opentrons/hardware_control/ot3api.py @@ -33,9 +33,6 @@ pipette_load_name_conversions as pipette_load_name, ) from opentrons_shared_data.robot.dev_types import RobotType -from opentrons_shared_data.errors.exceptions import ( - StallOrCollisionDetectedError, -) from opentrons import types as top_types from opentrons.config import robot_configs @@ -1531,19 +1528,12 @@ async def _home_axis(self, axis: Axis) -> None: axis_home_dist = 20.0 if origin[axis] - target_pos[axis] > axis_home_dist: target_pos[axis] += axis_home_dist - try: - await self._backend.move( - origin, - target_pos, - speed=400, - stop_condition=HWStopCondition.none, - ) - except StallOrCollisionDetectedError: - self._log.warning( - f"Stall on {axis} during fast home, encoder may have missed an overflow" - ) - await self.refresh_positions(acquire_lock=False) - + await self._backend.move( + origin, + target_pos, + speed=400, + stop_condition=HWStopCondition.none, + ) await self._backend.home([axis], self.gantry_load) else: # both stepper and encoder positions are invalid, must home From 0f87da220d545eb28a021a0154f495a45635c480 Mon Sep 17 00:00:00 2001 From: Max Marrone Date: Thu, 14 Mar 2024 08:37:49 -0400 Subject: [PATCH 8/9] chore(release): Release notes for v7.2.1 (#14642) # Overview User-facing app and robot release notes for v7.2.1. Addresses RTC-405. # Review requests Coherent, correct, and complete? # Risk assessment No risk. --------- Co-authored-by: Edward Cormany --- api/release-notes.md | 13 +++++++++++++ app-shell/build/release-notes.md | 10 ++++++++++ 2 files changed, 23 insertions(+) diff --git a/api/release-notes.md b/api/release-notes.md index c680cef73ca..ff193247459 100644 --- a/api/release-notes.md +++ b/api/release-notes.md @@ -6,6 +6,19 @@ log][]. For a list of currently known issues, please see the [Opentrons issue tr --- +## Opentrons Robot Software Changes in 7.2.1 + +Welcome to the v7.2.1 release of the Opentrons robot software! + +### Bug Fixes + +- Fixed an issue where OT-2 tip length calibrations created before v4.1.0 would cause a "missing calibration data" error that you could only resolve by resetting calibration. +- Fixed collision prediction being too conservative in certain conditions on Flex, leading to errors even when collisions wouldn't take place. +- Flex now properly homes after an instrument collision. +- `opentrons_simulate` now outputs entries for commands that drop tips in the default trash container in protocols that specify Python API version 2.16 or newer. + +--- + ## Opentrons Robot Software Changes in 7.2.0 Welcome to the v7.2.0 release of the Opentrons robot software! diff --git a/app-shell/build/release-notes.md b/app-shell/build/release-notes.md index 410c27a58a4..97fa5f01b81 100644 --- a/app-shell/build/release-notes.md +++ b/app-shell/build/release-notes.md @@ -6,6 +6,16 @@ log][]. For a list of currently known issues, please see the [Opentrons issue tr --- +## Opentrons App Changes in 7.2.1 + +Welcome to the v7.2.1 release of the Opentrons App! + +### Bug Fixes + +- Fixed a memory leak that could cause the app to crash. + +--- + ## Opentrons App Changes in 7.2.0 Welcome to the v7.2.0 release of the Opentrons App! From 583dcf66acc5be8de46f87956f520921c49a224a Mon Sep 17 00:00:00 2001 From: Derek Maggio Date: Thu, 7 Mar 2024 12:30:28 -0800 Subject: [PATCH 9/9] fix(api): simulate not logging drop_tip with no args (#14606) Fixes [RESC-214](https://opentrons.atlassian.net/browse/RESC-214) - [x] Add simulate test that runs a protocol using `drop_tip` with no args - Add publisher.publish_context context manager when calling drop tip with no args - Add above test case to `test_simulate.py` - It seems that the simulate drop tip functionality could benefit from more robust test coverage. Should make sure that we validate all branches inside of drop_tip. But is this PR the place to do it? Very low. Just added a message and a test [RESC-214]: https://opentrons.atlassian.net/browse/RESC-214?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ --- .../opentrons/protocol_api/instrument_context.py | 13 ++++++++++--- api/tests/opentrons/data/ot2_drop_tip.py | 11 +++++++++++ api/tests/opentrons/test_simulate.py | 7 +++++++ 3 files changed, 28 insertions(+), 3 deletions(-) create mode 100644 api/tests/opentrons/data/ot2_drop_tip.py diff --git a/api/src/opentrons/protocol_api/instrument_context.py b/api/src/opentrons/protocol_api/instrument_context.py index 03b843e4ffa..e77d263d747 100644 --- a/api/src/opentrons/protocol_api/instrument_context.py +++ b/api/src/opentrons/protocol_api/instrument_context.py @@ -1017,9 +1017,16 @@ def drop_tip( if isinstance(trash_container, labware.Labware): well = trash_container.wells()[0] else: # implicit drop tip in disposal location, not well - self._core.drop_tip_in_disposal_location( - trash_container, home_after=home_after - ) + with publisher.publish_context( + broker=self.broker, + command=cmds.drop_tip_in_disposal_location( + instrument=self, location=trash_container + ), + ): + self._core.drop_tip_in_disposal_location( + trash_container, + home_after=home_after, + ) self._last_tip_picked_up_from = None return self diff --git a/api/tests/opentrons/data/ot2_drop_tip.py b/api/tests/opentrons/data/ot2_drop_tip.py new file mode 100644 index 00000000000..4d98ecda909 --- /dev/null +++ b/api/tests/opentrons/data/ot2_drop_tip.py @@ -0,0 +1,11 @@ +from opentrons import protocol_api + +requirements = {"robotType": "OT-2", "apiLevel": "2.16"} + + +def run(ctx: protocol_api.ProtocolContext) -> None: + tipracks = [ctx.load_labware("opentrons_96_tiprack_300ul", "5")] + m300 = ctx.load_instrument("p300_multi_gen2", "right", tipracks) + + m300.pick_up_tip() + m300.drop_tip() diff --git a/api/tests/opentrons/test_simulate.py b/api/tests/opentrons/test_simulate.py index b4a51838cce..6750bf850b0 100644 --- a/api/tests/opentrons/test_simulate.py +++ b/api/tests/opentrons/test_simulate.py @@ -90,6 +90,13 @@ def test_simulate_without_filename(protocol: Protocol, protocol_file: str) -> No "Dropping tip into H12 of Opentrons OT-2 96 Tip Rack 1000 µL on slot 1", ], ), + ( + "ot2_drop_tip.py", + [ + "Picking up tip from A1 of Opentrons OT-2 96 Tip Rack 300 µL on slot 5", + "Dropping tip into Trash Bin on slot 12", + ], + ), ], ) def test_simulate_function_apiv2_run_log(